flowfuse icon indicating copy to clipboard operation
flowfuse copied to clipboard

Support for annual billing option

Open knolleary opened this issue 1 year ago • 9 comments

Part of https://github.com/FlowFuse/customer/issues/233

We want to provide the option for a user to pay annually, rather than the existing monthly model. This issue is for the technical work needed to achieve that.

Current Model

  • A TeamType can have:
    • (A) a stripe product/price associated with it
    • (B) a stripe product/price associated with each instance type and device
  • Our Starter team has (A) - a monthly cost for the team to exist.
  • Our Team/Enterprise teams have (B) - no monthly cost for the team to exist, but cost per instance/device that exists (that varies based on the size of the instance)

Desired Model

The desired model described in the linked issue is to have a minimum cost for Team/Enterprise - and for annual billing to be available in order to apply a discounted rate.

We have two options for implementing that:

  1. Use the current model to add a stripe price/product to the Team/Enterprise tier teams. Then set the 'free' allocation of the available instance type to match what is included in that base price. This way the billing system won't do on-demand charging for the first X instances - only when they go above X. The problem with this approach is it doesn't allow the user to commit to more than the minimum number that the team price factors in. So whilst this may offer a quick solution, it doesn't get us closer to where we want to be.

  2. The better model is, when setting up billing, let the user pick the desired team type, then be shown a slider for how many instances they want included - with the minimum applied. When they checkout, we then add the appropriate number of instances items to the subscription. The billing system can then ensure the item count on the subscription does not go below the defined minimum.

Annual billing

Supporting annual billing option is fairly straight forward to do the basics:

  1. The current billing interval is determined by the stripe price. So the starting point is to create yearly price objects for each stripe product.
  2. The TeamType configuration (which holds tier-specific pricing for each instance type), needs extending to include the yearly price details (two fields to add: stripe price ID, and the price we show on the page) for the team.
  3. Add toggle on Upgrade Team page (where the user picks Team Tier), for monthly/yearly pricing.
  4. Add flag on api to indicate monthly/yearly billing choice

Problems to resolve

If a user subscribes with Annual billing for 5 instances, what happens when they create their 6th instance.

  • we currently add prorata usage to their next invoice. On a monthly schedule, that's acceptable - they pay for what they have used over the last month.
  • For annual billing, that model doesn't work: user subscribes on Jan 1st for 5 instances. On Jan 2nd they create 30 instances - we don't charge them until their next invoice a year from now.
  • The alternative is we invoice immediately for any billing changes beyond their annual commitment. But then we are then back to raising an invoice for each billable change the user makes. That is something we were specifically asked to stop doing.

We need to resolve the desired behaviour for on-demand usage that exceeds their initial subscription.

knolleary avatar Aug 02 '24 16:08 knolleary

I've looked at how other services approach this to see if there's a model we can easily adopt.

  • Figma - ref.
    • You can pay annually for X seats.
    • If you add additional seats, they are billed monthly
    • You are given an opportunity to convert monthly seats to yearly seats. They get added, pro-rata, to the annual subscription
    • 30 days prior to yearly renewal, they send a reminder email, giving you a chance to convert any monthly to yearly

With my mental model of Stripe subscriptions, this is a model I can relate to. Stripe doesn't let you have a single subscription with mixed billing cycles - so this looks like the account has separate annual and monthly subscriptions - with a chore of moving items from the monthly-sub to the annual-sub.

All of our billing code operates on the principle of a team having a single subscription. Having a model with two different subscriptions to manage will require a fair amount of work - both backend and frontend.

knolleary avatar Aug 06 '24 16:08 knolleary

@knolleary To keep invoice changes to a minimum when a customer is in an ongoing annual plan, I propose that customers on Team and Enterprise (1) start a new annual subscription whenever they add instances and (2) buy instances in bundles of 5.

(1) is for simplicity and because it is compatible with our current implementation of Stripe. Michael informs me that this is how Salesforce handles mid-term contract upgrades. (2) will minimize the number of contract changes that are needed. If a customer isn't sure whether they want to buy 12 instances or 14 instances, we inform them that they'll actually be paying for 15, and the next bump up is to 20 instances. This will allow them to allocate their flows appropriately, provide the opportunity for upsells, and minimize the number of invoicing changes that have to be made during a 12 month period.

gstout52 avatar Aug 19 '24 17:08 gstout52

@gstout52 what you are proposing means a team can have multiple subscriptions. That is a fundamental change to our billing system that has always worked on the basis that a team has a single subscription. It opens up a lot of edge cases to deal with - what happens if they fail to pay on one subscription but not another for example. I don't know if that can be achieved in the next 3 weeks with all of the other pieces we still need to do for the pricing changes.

knolleary avatar Sep 02 '24 09:09 knolleary

(1) start a new annual subscription whenever they add instances and (2) buy instances in bundles of 5.

The more I think about how this would work, the bigger the set of changes are.

Our current model is very simple; each time an instance is created/deleted, we validate the quantity of the corresponding stripe product matches what they have so they are charged for what they are using.

As discussed previously, I could see a path forward to have a minimum purchase when they create the team; have the $-amount of the team price be equivalent to X instances - where X is a fixed amount.

To support pre-purchasing bundles of instances is a significant change. We now need to track separately what they have purchased from what they are using. Plus all of the UX work around what happens if a user has paid for 5 instances and they want to create a 6th. In our current model, they just create the instance. But now we have to block that until they go via stripe to create a new subscription (entering in all their billing details again) to increase their allowed instance count.

knolleary avatar Sep 02 '24 10:09 knolleary

For this, let's start with providing annual billing on Starter only. It can serve as a first iteration, has value in improving ARR and reducing churn, and won't require answering some of these more challenging questions. What do you think, @knolleary ?

gstout52 avatar Dec 16 '24 14:12 gstout52

@knolleary I'm resurrecting this one, only for the Team tier. Starter churn is low, and we do not have a business reason for offering a price below $20/mo for Starter. So, focusing just on Team FFC, I believe the following is possible:

  1. Offer Team at an annual price equal to ($175 x 11) , for the platform fee itself. That constitutes a free month for annual subscribers.
  2. Continue to bill additional instances on a per-instance basis, as now.

gstout52 avatar Mar 03 '25 20:03 gstout52

@knolleary I added this to the Development board, but before scheduling it I'd like to understand the scope of work. If it sounds worthwhile, I'll then schedule it afterward.

gstout52 avatar Apr 12 '25 13:04 gstout52

I will spend some time today/tomorrow scoping out what is possible. We can then decide how we want to schedule the work.

knolleary avatar Apr 15 '25 10:04 knolleary

Interested Team customers:

Possibly Interested Team customers:

gstout52 avatar Apr 16 '25 18:04 gstout52

I think https://stackoverflow.com/questions/63076853/stripe-charge-changes-monthly-for-yearly-subscription outlines the approach we can take at the low level. I am going to try to run a manual test so I can document the invoicing behaviour it leads to. From there, I can scope the UI changes needed to drive it.

knolleary avatar May 14 '25 12:05 knolleary

Good news; I have tested this stripe behaviour and I think it is what we've been looking for.

To summarise:

  • we can create an annual subscription. It invoices immediately on day 0. The 'next' invoice is then scheduled 1 year from now.
  • whenever updates are made (instance added/removed) we can use pending_invoice_item_interval to say 'invoice this change in 1 month', rather than wait for the invoice on the 1 year anniversary.

With that understood, I can now scope/size the platform changes needed to support this.

  1. Add configuration options to TeamType to capture annual billing config (price/product/description) items
  2. Update UI to provide monthly/annual toggle. Not sure where that best fits in the flow of things - need to stare of the screens a bit more.
  3. Update billing table to reflect annual pricing choice.
  4. Update billing logic to apply the pending_invoice_item_interval setting. I need to run a test to see what happens if that is set on a monthly billing subscription; ie, can we get away with always setting it, or does it need to be conditional.

knolleary avatar May 14 '25 15:05 knolleary

@knolleary Checking on this.

gstout52 avatar May 29 '25 20:05 gstout52

We discussed this on the strategy call today. Concern was raised over the engineering time needed to do this fully, compared to the immediate upside it brings.

A smaller iteration was suggested to add an annual billing prompt in the UX that acts as a Contact Us call to action - so not fully self-service, but allows a team to raise their hand with Sales that they are interested in annual billing. I'll raise a separate item for that with a sizing attached.

knolleary avatar Jun 03 '25 14:06 knolleary

We discussed this on the strategy call today. Concern was raised over the engineering time needed to do this fully, compared to the immediate upside it brings.

A smaller iteration was suggested to add an annual billing prompt in the UX that acts as a Contact Us call to action - so not fully self-service, but allows a team to raise their hand with Sales that they are interested in annual billing. I'll raise a separate item for that with a sizing attached.

If the consensus is that self-service annual billing is too much work, I am concerned that this is not the solution because then we will have to put self-service customers on manual billing.

gstout52 avatar Jun 05 '25 13:06 gstout52

Thanks, @knolleary ! Passing along a question from ZJ: is it possible for the customer to purchase additional instances for the year and make that a part of the annual purchase? The goal would be to set a high water mark for usage, and provide a discount for the accordingly.

gstout52 avatar Jun 17 '25 19:06 gstout52

We're moving forward with Nick's proposed version of this, beginning with the Starter tier. This will reduce churn on the Starter tier, which directly correlates to increased MRR. We can consider a high water mark proposal down the line as a subsequent iteration.

gstout52 avatar Jul 02 '25 17:07 gstout52

I have raised #5896 that adds support for annual billing.

A user will be able to select monthly/annual billing for any TeamTypes that have been configured with Annual billing stripe prices.

They can chose the billing cycle at the following points:

  • Creating a new team
  • Whilst in trial and they choose to setup billing
  • Upgrading (or Downgrading) their existing subscription.

What this does not support currently is:

  • A user currently pays monthly and wants to move to annual at the same tier.

I will see if I can get that done before I'm out - otherwise, will get it raised as a follow-on issue for someone to pick up.

knolleary avatar Aug 08 '25 15:08 knolleary