Skip to content

Subscriptions: use djstripe for Plan and PlanFeature models #9313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
4 tasks
agjohnson opened this issue Jun 8, 2022 · 16 comments · Fixed by #10238
Closed
4 tasks

Subscriptions: use djstripe for Plan and PlanFeature models #9313

agjohnson opened this issue Jun 8, 2022 · 16 comments · Fixed by #10238
Assignees
Labels
Accepted Accepted issue on our roadmap Improvement Minor improvement to code

Comments

@agjohnson
Copy link
Contributor

Similar to #9312 we'll want to use djstripe for these models as well. This is a little bit more work, as there is no Stripe resource for PlanFeature.

  • Decide on mechanism for storing plan features programmatically -- options might include storing metadata values on the Plan instances
  • Update our feature detection code to use the new data and Stripe Plan objects
  • Again, monitor this heavily. This change might be more apt to break features for users. Be very careful of features for Pro plan users, which are the most likely to be gated behind PlanFeature conditionals. Any conditional feature should be tested before release or we risk bringing down user sites\
  • Bonus points if your plan can be feature flagged itself. Something to avoid releasing to production and affecting all high tier customers simultaneously.
@agjohnson agjohnson added Improvement Minor improvement to code Accepted Accepted issue on our roadmap labels Jun 8, 2022
@agjohnson agjohnson moved this to Planned in 📍Roadmap Jun 8, 2022
@ericholscher ericholscher moved this from Planned to In progress in 📍Roadmap Aug 30, 2022
@ericholscher
Copy link
Member

There is ongoing work here, but in other PR's.

@stsewd
Copy link
Member

stsewd commented Oct 27, 2022

We also need to be able to make this per-subscription,
this is we can create a custom plan for a given org that has some features.

Are we okay with creating a custom product/price on stripe for any custom plans we have or may have in the future?

If so, I have three solutions:

Using metadata

Stripe doesn't allow us to have nested metadata (only one top level key/value),
so we could have something like:

  • feature:private-docs: 1
  • feature:page-views: 99

The metadata would be attached to the Price stripe models.

The values are always strings,
so we would need to cast them as integers when dealing with them.

A query for example would be something like:

prices = Price.objects.filter(**{'metadata__feature:private-docs': '1'})
organizations = Organization.objects.filter(stripe_customer__subscriptions__items__price__metadata__foo='bar')

I don't see that we have queries that do comparisons (lt, gt, etc),
so we should be fine having them as strings.

Attaching a Price object to a rtd Plan object

Instead of having the features as metadata,
we will have the features as our current models.

This is

organizations = Organization.objects.filter(stripe_customer__subscriptions__items__price__rtd_plan__features__value=1)

The advantage is that we will be able to use the lt/gt operators and we don't have to cast the values.

Attaching a Price object to several FeaturePlan objects

This is the same as the previous solution,
but instead of having the Plan object attached to the Price plan,
we will have a one-to-many relationship to PlanFeature models.

This is

organizations = Organization.objects.filter(stripe_customer__subscriptions__items__price__rtd_features__value=1)

@ericholscher
Copy link
Member

@stsewd which solution do you think is best?

@stsewd
Copy link
Member

stsewd commented Oct 31, 2022

@stsewd which solution do you think is best?

I may be slightly in favor of A, since we won't be needing our own models, but it has its own disadvantages... I'm good with B and C too p:

A

  • We would be dealing with a json object that supports just one level of key/values, and values are strings.
  • Any additional information we may want to attach to a plan (price) would be as metadata in the stripe object
  • We won't need to keep any of our models around, since all the information would be on stripe
  • When adding or editing a feature, we would need to make use of stripe API (doing it from the web also works),
    we could create a helper for these operations.
  • This also means that we won't have like a central list of features, but this could be automated,
    and we shouldn't have that many prices.

B

  • We would be keeping two of our models (Plan and PlanFeature)
  • We have one extra level of indirection (Plan object)
  • If needed, we can attach extra information in the Plan object instead of using metadata
  • Editing or adding a feature is a matter of editing things from the admin
  • Would be easy to group all features, since all of them would be related to the rtd Plan object

C

  • Very similar to B
  • We would be keeping one of our models PlanFeature
  • Any additional information we may want to attach to a plan (price) would be as metadata in the stripe object
  • Editing or adding a feature is a matter of editing things from the admin
  • Features are grouped by stripe price, but we still have a central list of features.

@ericholscher
Copy link
Member

ericholscher commented Nov 1, 2022

Heh, gotcha. I do think trying to fit all our data into Stripe feels useful, but also constraining. I'd be curious to hear what @humitos and other folks think here. I saw a talk about migrated to djstripe, and they mentioned not including your plan data in Stripe to keep things simpler to manage, but I can't find a link to that currently.

@stsewd
Copy link
Member

stsewd commented Nov 17, 2022

Notes after reading https://www.saaspegasus.com/guides/django-stripe-integrate/

  • Have plan data in our DB is a great idea.
    They also mention having this as static data in our code,
    but for our case I think is better to have it in our DB.
  • Attach stripe subs and customer to the organization,
    is a good idea too (the guide doesn't mention anything about attaching multiple subscriptions,
    I think we are good by having multiple prices attached to the subscription if we want to add more
    things to a subscription, like extra builder).
  • Features/local product models are attached to Products (not prices)
  • They have examples of user.product.metadata,
    but they don't actually show how is that relationship done.

So, the solution that I'm thinking would be something like the described in the item B. And we can create new models or re-use the ones (they have a lot of information we don't need). And they would be attached to a product, not a price, this means we would need to create a new product with one price in it for custom subscriptions (like enterprise plans).

@ericholscher
Copy link
Member

I think we are good by having multiple prices attached to the subscription if we want to add more
things to a subscription, like extra builder).

We should document how to do this correctly, so we don't mess it up.

So, the solution that I'm thinking would be something like the described in the item B. And we can create new models or re-use the ones (they have a lot of information we don't need). And they would be attached to a product, not a price, this means we would need to create a new product with one price in it for custom subscriptions (like enterprise plans).

This sounds like a good approach. I don't have a strong feeling about new or reusing models, as long as it's easy to transfer the data. Glad the document was useful!

@humitos
Copy link
Member

humitos commented Nov 22, 2022

@ericholscher

I do think trying to fit all our data into Stripe feels useful, but also constraining.

I have the same feeling here. I would say that I like having all the data in Stripe to avoid having to sync things, but at what cost? I understand that item A makes the usage of all of this more complicated/complex.

It seems that B is a middle ground solution here that gives us the best of the two worlds without complicating things too much. However, I'm not 100% sure to understand all the goals and constraints here.

What's the main goal of this task? I'm assuming that we want to be able to answer the questions "does this organization pays for Pro plan?" (e.g. when checking for Google SSO) and "does this organization pays for "extra builders"? If so, how many it pays for?" (e.g. when checking for concurrency limit). Am I correct here?

@stsewd

Features/local product models are attached to Products (not prices)

I think this makes sense, because a product will be "Basic Plan" and a price is "Yearly" / "Monthly", where prices do not affect the features the organization has available, so 👍🏼

Notes after reading saaspegasus.com/guides/django-stripe-integrate

I skimmed this document, but it's too big. Do you have a specific section you would recommend that's related to what we are talking here?

I think I found the important section https://www.saaspegasus.com/guides/django-stripe-integrate/#adding-metadata-to-your-stripe-objects

They also mention having this as static data in our code,
but for our case I think is better to have it in our DB.

After reading the section that I linked previously, I'm leaning towards hard code this in our code. It will be easier to follow the code, know exactly what are all the extra features we have and all our environments will be in sync. The use of dataclasses makes it really easy to follow. I don't expect require making changes on production about these plan features "on the fly", so I think it makes sense to have them hardcoded to reduce the complexity. Besides, we won't have issues about missing relationships between a Plan and a PlanFeature (as I remember having with the current code)

In case you disagree here, I'd like to know more about why do you think that for our case it's better to have these relations in the database.

@stsewd
Copy link
Member

stsewd commented Nov 22, 2022

What's the main goal of this task? I'm assuming that we want to be able to answer the questions "does this organization pays for Pro plan?" (e.g. when checking for Google SSO) and "does this organization pays for "extra builders"? If so, how many it pays for?" (e.g. when checking for concurrency limit). Am I correct here?

that's the goal, yes.

I skimmed this document, but it's too big. Do you have a specific section you would recommend that's related to what we are talking here?

I could recommend some sections

(that may be must part of the post...)

I don't expect require making changes on production about these plan features "on the fly"

We do need to make such changes when adding enterprise plans, since we would be creating a new product with another set of features.

@humitos
Copy link
Member

humitos commented Nov 23, 2022

@stsewd

I don't expect require making changes on production about these plan features "on the fly"

We do need to make such changes when adding enterprise plans, since we would be creating a new product with another set of features.

I'd say these don't happen too frequent to absorb the overhead of managing all these relationships and complexity for the most common cases. Usually, Enterprise plans take some weeks/months to finally get approved and paid --which will give us enough time to do a deploy with these changes. We can also do hotfix if we have to.

@ericholscher
Copy link
Member

After reading this, I think I'm mostly OK with hard-coding it. I do think the ability to have the plans in sync between development is useful, but I don't know how useful. But I do think that most Enterprise users have a standard set of requirements that they need. If we find that we're having to do hotfix deploys very frequently though, we should re-evaluate this decision.

I'm also 👍 on keeping it in the code if it's already written, and hard-coding is going to require extra work. I don't think there's a strong reason for either approach, so I'm mostly just advocating for the quickest option :)

@stsewd
Copy link
Member

stsewd commented Jan 18, 2023

Before making the big change, I want to unify the way we check for features #8920, that way it is easier to change the logic in a single place.

@agjohnson
Copy link
Contributor Author

@stsewd just to confirm, in the plan so far, we'd still retain our plans as products with multiple prices, correct? The checkout UI is a bit nicer in this case, compared to the legacy version of this, where each plan price is it's own product.

@stsewd
Copy link
Member

stsewd commented Jan 23, 2023

@stsewd just to confirm, in the plan so far, we'd still retain our plans as products with multiple prices, correct? The checkout UI is a bit nicer in this case, compared to the legacy version of this, where each plan price is it's own product.

yes, we will have multiple products with several prices, but features will be attached to the product, not the price.

@ericholscher
Copy link
Member

Blocked on #8920

@ericholscher
Copy link
Member

Gonna push this back, since it's not seeming to be priority.

@stsewd stsewd moved this from In progress to Needs review in 📍Roadmap Apr 19, 2023
@stsewd stsewd moved this from Needs review to In progress in 📍Roadmap Jul 6, 2023
@github-project-automation github-project-automation bot moved this from In progress to Done in 📍Roadmap Aug 10, 2023
stsewd added a commit that referenced this issue Aug 10, 2023
Instead of relying on our subscription models, we are now relying on the djstripe models, but the features of each subscription are still attached to our subscription models. In the new modeling, we have features attached to stripe products.

I'm using some data classes to make the representation instead of using plain dictionaries, they are more verbose than a plain dictionary, but are easier to manipulate in the code.

After this change has been deployed, we won't be needing our subscription models anymore. The available products are defined in .com, since in .org we don't have subscriptions. If we feel like we can have this in the DB, we can also switch the data classes to be Django models.

Closes #9313
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Accepted Accepted issue on our roadmap Improvement Minor improvement to code
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

4 participants