-
Notifications
You must be signed in to change notification settings - Fork 63
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
Conversation
Codecov Report
@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
|
NOw investigating when postgres hits failures that sqlite doesn't. Not immediately obvious the difference. |
Postgres tests should be fixed with the most recent commit. The tests were doing a |
} | ||
// This will perform all checks needed to ensure this instance | ||
// can be started - throws err if not | ||
await project.Team.checkInstanceStartAllowed(project) |
There was a problem hiding this comment.
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
?
1dcc7a1
to
36c2439
Compare
I have just rebased to |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's a PT
?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 () { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 }] }) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 }) |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
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 |
Changes were introduced in #2519
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 UIThe
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 typesConfig 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:
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: