Skip to content
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

Add support for multiple TeamTypes with limits/billing set per type #2519

Merged
merged 19 commits into from
Jul 26, 2023

Conversation

knolleary
Copy link
Member

@knolleary knolleary commented Jul 24, 2023

Description

Part of #2358

This expands the TeamTypes model to support the ability to have different TeamTypes with different limits/billing applied.

It is a significant piece of work as it required a lot of changes to how billing is structured given the configuration now has to come based on the TeamType.

I have structured the commits to try to make a bit more reviewable.

DB Changes

  • TeamType - renames 'enabled' to 'active' to be consistent with 'ProjectTypes'
  • TeamType - adds 'order' column so the types can be listed in the desired order in the UI

The Team model has had a lot of instance functions added to it to access information about the team. This is a much cleaner approach. When running EE with billing, some of those functions are overloaded to add billing-specific checks and logic. This has removed the need for a lot of billing checks in the base CE code.

API Changes

  • /api/v1/team-types/*** - apis added to create/update/list/delete team types

Config changes

Currently all team pricing config is provided in the yml file. With this PR, that info can now be set by admin in the UI.

When creating a TeamType, the admin has to provide a lot of information:

  • Stripe Price/Product for the TeamType itself
  • For each InstanceType on the platform - whether it is available to that Team, any limit on how many, a 'free allowance', and stripe price/product info
  • Device price/product
  • User limits (if any)
  • Trial mode options
  • Feature flags - although these are not currently honoured.

When the platform starts, it checks for the existing default 'starter' TeamType and adds if it necessary. If billing is enabled, it also adds the new Starter and Team (previously called Premium...) types - but leaves then inactive so the platform admin can choose when to enabled them (once they have finished configuring them).

Trial mode

Previously, trial mode was configured in admin settings - setting duration and what InstanceType is available. That configuration has moved to the individual TeamTypes configuration.

In Admin settings, if the 'create personal team' option is enabled, there is now a choice of what TeamType that personal team should be. It will default to the existing 'starter' team until configured otherwise.


What is missing before we can fully launch the new tiers in FF Cloud:

  • Upgrade path. There is currently no way for a team to change its type.
  • Feature limits - the UI lets the admin tick boxes to say what features should be enabled for a given type, but currently there are no restrictions applied
  • UX work around team creation/upgrade/billing. We have designs, but they are not yet implemented (and couldn't be until this base work was done).

@knolleary knolleary requested a review from Pezmc July 24, 2023 11:25
@codecov
Copy link

codecov bot commented Jul 24, 2023

Codecov Report

Merging #2519 (fe4a9db) into main (65703f1) will decrease coverage by 0.37%.
Report is 4 commits behind head on main.
The diff coverage is 47.52%.

@@            Coverage Diff             @@
##             main    #2519      +/-   ##
==========================================
- Coverage   40.20%   39.83%   -0.37%     
==========================================
  Files         491      494       +3     
  Lines       17319    17690     +371     
  Branches     4020     4125     +105     
==========================================
+ Hits         6963     7047      +84     
- Misses      10356    10643     +287     
Flag Coverage Δ
backend 74.47% <81.07%> (-0.16%) ⬇️
frontend 1.59% <0.00%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Changed Coverage Δ
forge/db/index.js 60.78% <ø> (-7.08%) ⬇️
...igrations/20220808-02-create-default-team-types.js 0.00% <ø> (-88.89%) ⬇️
...orge/db/migrations/20230711-01-update-team-type.js 0.00% <0.00%> (ø)
forge/db/models/Application.js 100.00% <ø> (ø)
forge/db/models/Device.js 76.19% <ø> (ø)
forge/db/models/Project.js 96.52% <ø> (ø)
forge/db/models/ProjectSettings.js 100.00% <ø> (ø)
forge/db/views/Team.js 86.36% <ø> (ø)
.../ee/lib/billing/emailTemplates/TrialTeamCreated.js 100.00% <ø> (ø)
...ge/ee/lib/billing/emailTemplates/TrialTeamEnded.js 100.00% <ø> (ø)
... and 36 more

... and 3 files with indirect coverage changes

@knolleary
Copy link
Member Author

NOw investigating when postgres hits failures that sqlite doesn't. Not immediately obvious the difference.

@knolleary
Copy link
Member Author

Postgres tests should be fixed with the most recent commit. The tests were doing a TeamType.findOne() to get the default team type - which is fine when there is only one. However, if billing is enabled, there will now be 3 team types - old starter, new Starter and new Premium. So the tests needed to be updated to ensure they got the old starter type.

}
// This will perform all checks needed to ensure this instance
// can be started - throws err if not
await project.Team.checkInstanceStartAllowed(project)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That this needs a comment to explain how the method works each time feels like a code smell to me.

How about naming it ensureInstanceStartAllowed, or throwIfInstanceStartNotAllowed?

@knolleary
Copy link
Member Author

I have just rebased to main and force pushed. This ensures the migration files are all present and correct in this branch following the merge of the pipeline work.

Copy link
Contributor

@Pezmc Pezmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments thus far, am about half way through a re-review. Just eating lunch!

description: teamType.description,
properties
}
if (includeAdminOnlyProps && teamType.get) {
// For some API calls, the teamType passed here is a raw Object, not
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change this so it's always a teamType object? Why do we need to support both?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this isn't ideal. I can't remember the specific code path that lead to needing to do this, but it was sufficiently non-trivial to resolve quickly that I worked around it.

filter = { active: false }
}
const teamTypes = await app.db.models.TeamType.getAll(paginationOptions, filter)
teamTypes.types = teamTypes.types.map(pt => app.db.views.TeamType.teamType(pt, request.session.User.admin))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a PT?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A remnant of when I copy and pasted the boilerplate of a CRUD api from the ProjectTypes api.

const where = { TeamId: this.id }
if (projectTypeId) {
if (typeof projectTypeId === 'string') {
projectTypeId = M.ProjectType.decodeHashid(projectTypeId)
} else if (projectTypeId.id) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should throw rather than silently fixing the error, that's bound to lead to confusion down the line. Is there a reason we need to support calling instanceCount passing in an object?

* Depending on the route taken, it is possible this property has not
* been fully loaded. This does the work to ensure it is there if needed.
*/
ensureTeamTypeExists: async function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm not clear on why everywhere wouldn't just use sequelizes await this.getTeamType() in all places, rather than having this logic to check whether or not TeamType was included when the team model was loaded.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment below on why handling TeamTypeId being undefined is needed - if not ideal.

*/
ensureTeamTypeExists: async function () {
if (!this.TeamTypeId) {
await this.reload({ include: [{ model: M.TeamType }] })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If TeamTypeId is undefined, why would reloading the Team model make a difference here? Is there somewhere we only load the models but with not all the fields?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were multiple places where we load a model and include Team, but limit it to the minimum of team attributes that are needed in that context. That would mean it didn't have TeamTypeId because it wasn't one of the selected attributes.

The reload ensures the full Team object is retrieved and the TeamType can be included.

I did go through a lot of the relevant models to ensure they included TypeTypeId, but wasn't 100% confident there wasn't still a reload lurking somewhere that would get missed. This is a safety net just in case.

* properties set
* @param {object} instanceType - a fully populated ProjectType object
*/
checkInstanceTypeCreateAllowed: async function (instanceType) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I dropped this on the previous PR, but I'd expect a function called check to return a boolean, rather than throwing an error.

Naming this something more explicit like: raiseIfInstanceTypeCreateNotAllowed or requireInstanceTypeCreateAllowed gives a better clue that it works by raising exceptions.

const teamDeviceLimit = team.TeamType.getProperty('deviceLimit')
if (typeof teamDeviceLimit === 'number') {
const teamDeviceLimit = await team.getDeviceLimit()
if (teamDeviceLimit > -1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal preference >= 0 (though is 0 valid?)

return
}
try {
// This will perform all checks needed to ensure this instance
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reading these comments explaining what the function does is definitely a code smell, a clearer name should make them redundant!

label: tt.name
}
})
this.teamTypes.sort((A, B) => { return A.order - B.order })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually this would go in a computed function, but we're not mutating teamTypes at any point, so this is ok for now.

Copy link
Contributor

@Pezmc Pezmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a long PR. Nick and I have spoken on slack about some of the nit-picks and follow ups to improve readability down the line, but no functionality changes are needed; so we're happy for this to be merged (for testing on staging) and them addressed as follow ups.

@knolleary knolleary merged commit 8da2139 into main Jul 26, 2023
4 of 5 checks passed
@knolleary knolleary deleted the 2359-teamtypes-api branch July 26, 2023 12:56
@Pezmc
Copy link
Contributor

Pezmc commented Aug 2, 2023

I hit a few edge cases with this while testing #2553, all centered around adding a new instanceType after the team type has been created.

Fixes and test coverage are in 49efe7e and 40f6507, but we should take extra care to verify editing team types before and after adding new instanceTypes on staging.

/CC: @MarianRaphael

Pezmc added a commit that referenced this pull request Oct 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

2 participants