Skip to content

Commit

Permalink
Added support API for force-cancelling subscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
jezzsantos committed Jun 22, 2024
1 parent 56cc30d commit ed6e9e2
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 40 deletions.
87 changes: 53 additions & 34 deletions docs/how-to-guides/900-migrate-billing-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,47 +18,47 @@ Depending on the BMS you select, they will each have their own conceptual models

The first step is to learn about how your customers are modeled in the BMS software.

For example, in Chargebee, your customer (i.e., `Organization` + "Buyer" in the product) will be modeled as a Chargebee Customer record, and a billing subscription (i.e., `Subscription` in the product) is modeled as a Chargebee Subscription. Chargebee has a notion of a Plan, but that has no equivalent concept in the product.
For example, in Chargebee, your customer (i.e., `Organization` + "Buyer" in the product) will be modeled as a Chargebee Customer record, and a billing subscription (i.e., `Subscription` in the product) is modeled as a Chargebee Subscription. Chargebee has a notion of a Plan, but that has no real equivalent concept in the product, except for the `TierId` and the `PlanId`.

Other BMS will have slightly different conceptual models, and you will need to understand them first.
Other BMS will have slightly different conceptual models, and you will need to understand them first, and how they map to the concepts in the product.

You will also need to configure the rules and other policies in the BMS first, before you start configuring your customer data.
You will also need to configure the basic rules and other policies in the BMS first, before you start configuring your customer data.

For example, configure API Keys, Webhooks etc.

The last thing will be to explore whether the BMS supports a "sandbox" environment for you to play around with and test your migration. You don't want to be adding test data to your production customer data.

> You may need to sign up for a free plan first to play around with and explore the options, and then use a paid version for your production data.
### View the available data

Use the API endpoint `GET /subscriptions/export` to view the data available for export into your chosen BMS.

> Note: this is a protected endpoint that you can only access with HMAC secrets
This data represents all the subscriptions created in the product so far, and you will need to import them into your chosen BMS.
This data represents all the subscriptions created in the product so far.

This is the data you will need to import into your chosen BMS, during the migration.

> Note: some of the values are simply encoded JSON values
```json
{
"subscriptions": [{
"id": "subscription_QR5hju7FDMIklw39GVCs",
"buyerId": "user_wYU128873MRRBRtjj3E",
"owningEntityId": "org_M36utr98Fdde8890BDEcS2",
"providerName": "simple_billing_provider",
"providerState": {
"SubscriptionId": "simplesub_dd45baa2188c43d39745344356781123"
},
"id": "subscription_QR5hju7FDMIklw39GVCs",
"buyer": {
"buyerId": "user_wYU128873MRRBRtjj3E",
"owningEntityId": "org_MM36utr98Fdde8890BDEcS2",
"id": "user_wYU128873MRRBRtjj3E",
"companyReference": "org_MM36utr98Fdde8890BDEcS2",
"firstName": "firstname",
"lastName": "lastname",
"name": "{\"FirstName\":\"afirstname\",\"LastName\":\"alastname\"}",
"emailAddress": "[email protected]",
"address": {
"line1": "",
"line2": "",
"line3": "",
"city": "",
"state": "",
"countryCode": "NZL",
"zip": ""
}
"address": "{\"City\":\"\",\"CountryCode\":\"NZL\",\"Line1\":\"\",\"Line2\":\"\",\"Line3\":\"\",\"State\":\"\",\"Zip\":\"\"}"
},
}
],
Expand All @@ -78,31 +78,47 @@ This data represents all the subscriptions created in the product so far, and yo

### Build Your Scripts

You will likely need to build some scripts that take the raw data above and automate the creation of various related data structures in the new BMS.
You will likely need to build some scripts that translate the raw data above and automate the creation of various related data structures in the new BMS.

You will need to test and refine these scripts thoroughly so that they are reliable when run during the migration with many more subscriptions.
For example, in Chargebee:

### Configure Your New Plans
1. You would create a Chargebee Customer record using the data in the `buyer` property. You would save the `buyer.id` in the metadata of the Chargebee Customer record.
2. You would create a Chargebee Subscription for the Chargebee Customer. You would also save the `id` and `owningEntityId` as metadata in the Chargebee Subscription.
3. You would define some Chargebee Plans, and assign one of those plans to the Chargebee subscription.

In your chosen BMS, you will need to design and define the new pricing plans you intend to support for all your customers moving forward in this BMS.
Next, during the migration, once you have automated the creation of the BMS records, you will also need a collection of metadata of those BMS records back into the data of the `IBillingProvider` for when it is being used.

You won't see any plan information in the exported data from the previous step, unless it appears in the `ProviderState` entry.
Use the API endpoint `POST /subscriptions/{Id}/migrate` to copy the data from your BMS into the product.

> If you are using the `SimpleBillingProvider` prior to this step, it does not maintain any plan information. That's because it hardcodes a single plan for everyone's use. You can find that single hardcoded plan information in the `InProcessInMemSimpleBillingGatewayService`.
For example, for Chargebee, we would be saving, at the very least, the following data:

You also need to remember that the `SimpleBillingProvider` has everyone on a "free" plan that requires no payment method and does not support a Trial period.
1. The Chargebee `CustomerId`
2. The Chargebee `SubscriptionId` and `SubscriptionStatus`
3. The Chargebee `PlanId`

This means that when you import these subscriptions (created by the `SimpleBillingProvider`) into your new BMS, you need to import them into a "free" plan that also does not require them to have a valid `PaymentMethod`. Your customers will not have given their `PaymentMethod` yet.
> There are a total of about 15 other properties about the Customer, Subscription, Plan, and the payment method that will be stored by the `IBillingProvider` over the course of the lifetime of a subscription. See the implementation of the `ChargebeeBillingProvider`.
Also, in the product, by default, we have defined the following `SubscriptionTier`:
Next, you would need to test and refine these scripts thoroughly so that they are reliable when run during the migration with hundreds/thousands of subscriptions (depending on how many customers you have at the time).

### Configure Your New Plans

In your chosen BMS, you will need to design and define the new pricing plans you intend to support for all your customers moving forward in this BMS.

> If you are using the `SimpleBillingProvider` prior to this step, you won't see much plan information in the exported data from the previous step, that's because this provider does not maintain much plan information at all. That's because it hardcodes a single plan for everyone's use. You can find that single hardcoded plan information in the `InProcessInMemSimpleBillingGatewayService`.
>
> You also need to remember that the `SimpleBillingProvider` has everyone on a "free" plan that requires no payment method and does not support a Trial period.
This means that when you import these subscriptions (created by the `SimpleBillingProvider`) into your new BMS, you need to import them into a "free" plan that does not require them to have a valid `PaymentMethod`. If migrating from the `SimpleBillingProvider`, your customers will not have given any `PaymentMethod` yet.

In the product, by default, we have defined the following tiers (see: `SubscriptionTier`):

* Standard
* Professional
* Enterprise

You are free to rename, add, or remove these tiers (in the code) to whatever you would like to support in your future pricing plans in your new BMS.
You are free to rename, add, or remove these tiers (in the code) to whatever you would like to support in your future pricing plans in your new BMS. Essentially, we have 3 paid tiers, where `Standard` may have a trial, and is generally the default plan for new users.

> Remember to modify the mapping between these tiers and the feature levels you will be supporting in your pricing plans.
> Remember, if you modify these tiers, you will also need to modify the mapping between these tiers and the feature levels you will be supporting in your pricing plans. see the `EndUserRoot` for details.
In your BMS, we recommend defining at least the following plans:

Expand All @@ -117,11 +133,13 @@ You will need to define all the parameters for each of these new plans, includin

### Configure the BillingProvider

Your newly chosen BMS will require a built and tested implementation of the `IBillingProvider` to work with it. You will also need to have built any webhooks or synchronization processes built to handle updates originating from the BMS to the `Subscriptions` subdomain.
Your newly chosen BMS will require a built and tested implementation of the `IBillingProvider` to work with it.

You will also need to build any webhooks or synchronization processes to handle updates originating from the BMS to the `Subscriptions` subdomain so that changes in the BMS update the data kept in the `Subscription` of the product.

> SaaStack comes with a small number of existing `IBillingProvider` implementations already. These can be used and they can be referenced to build your own implementations for other BMSs.
To swap out the existing `IBillingProvider` (e.g. `SimpleBillingProvider`) with your new implementation, you simply change the dependency injection code in the `Subscriptions` subdomain to swap them out.
To swap out the existing `IBillingProvider` (e.g. `SimpleBillingProvider`) with your new implementation, you simply change the dependency injection code in the `Subscriptions` subdomain (see: `SubscriptionsModule`).

> You can then delete the `SimpleBillingProvider` and its associated classes and tests. You are very unlikely to revert back to using it ever again.
Expand All @@ -142,10 +160,11 @@ Unfortunately, due to the nature of this migration, you are going to need to sch
These are the activities to schedule and perform to complete the migration and before you can resume service with the new BMS integration:

1. Export the data from the running product using the endpoint: `GET /subscriptions/export`
2. Immediately, shutdown your service, to prevent any new customers signing up (and thus creating new subscriptions). You may need other measures if you have heavy sign-up traffic to ensure no one is signing up while you are exporting the data.
3. Import your exported subscription data into your new BMS. You are likely going to be scripting the creation a numerous new data structures in the new BMS, with this data.
2. Immediately shutdown your service, to prevent any new customers signing up (and thus creating new subscriptions). You may need other measures if you have heavy sign-up traffic to ensure no one is signing up while you are exporting the data.
3. Import the exported data it into your new BMS (using your scripts and the BMS API). You are likely going to be scripting the creation of numerous new data structures in the new BMS with this data, and collecting a bunch of key identifiers that are created.
4. Deploy a new version of your software that includes the configured new `IBillingProvider` to your new BMS.
5. Resume service using your new BMS integration, and test by signing up new users and ensuring that new subscriptions are created in your new BMS. This may be performed in a non-production slot.
6. Resume service for all your customers.
5. With the collected data from the BMS, import that data back into your API (using `POST /subscriptions/{Id}/migrate`)
6. Resume service using your new BMS integration, and test by signing up new users and ensuring that new subscriptions are created in your new BMS. This may be performed in a non-production slot.
7. Resume service for all your customers.

After this migration, any new users that are registered in your product will be automatically integrated into your product, and appear in the BMS.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Infrastructure.Web.Api.Interfaces;

namespace Infrastructure.Web.Api.Operations.Shared.Subscriptions;

/// <summary>
/// Forces the billing subscription to be cancelled for the organization.
/// </summary>
[Route("/subscriptions/{Id}/force", OperationMethod.Delete, AccessType.Token)]
[Authorize(Roles.Platform_Operations)]
public class ForceCancelSubscriptionRequest : UnTenantedRequest<GetSubscriptionResponse>, IUnTenantedOrganizationRequest
{
public string? Id { get; set; }
}
3 changes: 3 additions & 0 deletions src/SubscriptionsApplication/ISubscriptionsApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ Task<Result<SubscriptionWithPlan, Error>> ChangePlanAsync(ICallerContext caller,
Task<Result<SearchResults<SubscriptionToMigrate>, Error>> ExportSubscriptionsToMigrateAsync(ICallerContext caller,
SearchOptions searchOptions, GetOptions getOptions, CancellationToken cancellationToken);

Task<Result<SubscriptionWithPlan, Error>> ForceCancelSubscriptionAsync(ICallerContext caller, string owningEntityId,
CancellationToken cancellationToken);

Task<Result<SubscriptionWithPlan, Error>> GetSubscriptionAsync(ICallerContext caller, string owningEntityId,
CancellationToken cancellationToken);

Expand Down
22 changes: 19 additions & 3 deletions src/SubscriptionsApplication/SubscriptionsApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,22 @@
using Common.Extensions;
using Domain.Common.Identity;
using Domain.Common.ValueObjects;
using Domain.Shared;
using Domain.Shared.Subscriptions;
using SubscriptionsApplication.Persistence;
using SubscriptionsDomain;
using Subscription = SubscriptionsApplication.Persistence.ReadModels.Subscription;
using Validations = SubscriptionsDomain.Validations;

namespace SubscriptionsApplication;

public partial class SubscriptionsApplication : ISubscriptionsApplication
{
private readonly IBillingProvider _billingProvider;
private readonly IIdentifierFactory _identifierFactory;
private readonly ISubscriptionOwningEntityService _subscriptionOwningEntityService;
private readonly IRecorder _recorder;
private readonly ISubscriptionRepository _repository;
private readonly ISubscriptionOwningEntityService _subscriptionOwningEntityService;
private readonly IUserProfilesService _userProfilesService;

public SubscriptionsApplication(IRecorder recorder, IIdentifierFactory identifierFactory,
Expand Down Expand Up @@ -120,6 +122,12 @@ Task<Result<SubscriptionMetadata, Error>> OnTransfer(BillingProvider provider, I
}
}

public async Task<Result<SubscriptionWithPlan, Error>> ForceCancelSubscriptionAsync(ICallerContext caller,
string owningEntityId, CancellationToken cancellationToken)
{
return await CancelSubscriptionAsync(caller, owningEntityId, cancellationToken);
}

public async Task<Result<SubscriptionWithPlan, Error>> GetSubscriptionAsync(ICallerContext caller,
string owningEntityId, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -213,8 +221,15 @@ public async Task<Result<SubscriptionWithPlan, Error>> CancelSubscriptionAsync(I

var subscription = retrieved.Value;
var cancellerId = caller.ToCallerId();
var cancellerRoles = Roles.Create(caller.Roles.All);
if (cancellerRoles.IsFailure)
{
return cancellerRoles.Error;
}

var cancelled =
await subscription.CancelSubscriptionAsync(_billingProvider.StateInterpreter, cancellerId, CanCancel,
await subscription.CancelSubscriptionAsync(_billingProvider.StateInterpreter, cancellerId,
cancellerRoles.Value, CanCancel,
OnCancel,
false);
if (cancelled.IsFailure)
Expand Down Expand Up @@ -403,7 +418,8 @@ async Task<Permission> CanView(SubscriptionRoot subscription1, Identifier viewer

/// <summary>
/// Calculate the date range based on inputs and defaults
/// Note: If not explicitly specified, the range should be <see cref="Validations.Subscription.DefaultInvoicePeriod" />
/// Note: If not explicitly specified, the range should be
/// <see cref="SubscriptionsDomain.Validations.Subscription.DefaultInvoicePeriod" />
/// in length, and as muh as possible include those past months
/// </summary>
private static (DateTime From, DateTime To) CalculatedSearchRange(DateTime? fromUtc, DateTime? toUtc)
Expand Down
Loading

0 comments on commit ed6e9e2

Please sign in to comment.