(core) add free team site product

Summary:
This adds a Feature object that is an approximation of what we
plan for free team sites. It includes restrictions that are not
yet implemented, and an endpoint for testing.

Test Plan: added a test

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3243
This commit is contained in:
Paul Fitzpatrick 2022-02-02 14:30:50 -05:00
parent 64abfcb0ac
commit 4890a1fe89
4 changed files with 43 additions and 3 deletions

View File

@ -34,6 +34,18 @@ export interface Features {
// (default: unlimited) // (default: unlimited)
readOnlyDocs?: boolean; // if set, docs can only be read, not written. readOnlyDocs?: boolean; // if set, docs can only be read, not written.
snapshotWindow?: { // if set, controls how far back snapshots are kept.
count: number; // TODO: not honored at time of writing.
unit: 'month'|'year';
};
baseMaxRowsPerDocument?: number; // If set, establishes a default maximum on the
// number of rows (total) in a single document.
// Actual max for a document may be higher.
// TODO: not honored at time of writing.
// TODO: nuances about how rows are counted.
baseMaxApiUnitsPerDocumentPerDay?: number; // Similar for api calls.
} }
// Check whether it is possible to add members at the org level. There's no flag // Check whether it is possible to add members at the org level. There's no flag

View File

@ -72,11 +72,15 @@ export function addOrg(
dbManager: HomeDBManager, dbManager: HomeDBManager,
userId: number, userId: number,
props: Partial<OrganizationProperties>, props: Partial<OrganizationProperties>,
options?: {
planType?: 'free'
}
): Promise<number> { ): Promise<number> {
return dbManager.connection.transaction(async manager => { return dbManager.connection.transaction(async manager => {
const user = await manager.findOne(User, userId); const user = await manager.findOne(User, userId);
if (!user) { return handleDeletedUser(); } if (!user) { return handleDeletedUser(); }
const query = await dbManager.addOrg(user, props, { const query = await dbManager.addOrg(user, props, {
...options,
setUserAsOwner: false, setUserAsOwner: false,
useNewPlan: true useNewPlan: true
}, manager); }, manager);

View File

@ -24,6 +24,21 @@ export const teamFeatures: Features = {
maxSharesPerDoc: 2 maxSharesPerDoc: 2
}; };
/**
* A summary of features available in free team sites.
* At time of writing, this is a placeholder, as free sites are fleshed out.
*/
export const teamFreeFeatures: Features = {
workspaces: true,
vanityDomain: true,
maxSharesPerWorkspace: 0, // all workspace shares need to be org members.
maxSharesPerDoc: 2,
maxDocsPerOrg: 20,
snapshotWindow: { count: 1, unit: 'month' },
baseMaxRowsPerDocument: 5000,
baseMaxApiUnitsPerDocumentPerDay: 5000
};
/** /**
* A summary of features used in unrestricted grandfathered accounts, and also * A summary of features used in unrestricted grandfathered accounts, and also
* in some test settings. * in some test settings.
@ -101,6 +116,10 @@ const PRODUCTS: IProduct[] = [
name: 'suspended', name: 'suspended',
features: suspendedFeatures, features: suspendedFeatures,
}, },
{
name: 'teamFree',
features: teamFreeFeatures,
},
]; ];
/** /**
@ -112,6 +131,7 @@ export function getDefaultProductNames() {
teamInitial: 'stub', // Team site starts off on a limited plan, requiring subscription. teamInitial: 'stub', // Team site starts off on a limited plan, requiring subscription.
teamCancel: 'suspended', // Team site that has been 'turned off'. teamCancel: 'suspended', // Team site that has been 'turned off'.
team: 'team', // Functional team site. team: 'team', // Functional team site.
teamFree: 'teamFree',
}; };
} }

View File

@ -1077,11 +1077,14 @@ export class HomeDBManager extends EventEmitter {
* @param useNewPlan: by default, the individual billing account associated with the * @param useNewPlan: by default, the individual billing account associated with the
* user's personal org will be used for all other orgs they create. Set useNewPlan * user's personal org will be used for all other orgs they create. Set useNewPlan
* to force a distinct non-individual billing account to be used for this org. * to force a distinct non-individual billing account to be used for this org.
* @param planType: if set, controls the type of plan used for the org. Only
* meaningful for team sites currently.
* *
*/ */
public async addOrg(user: User, props: Partial<OrganizationProperties>, public async addOrg(user: User, props: Partial<OrganizationProperties>,
options: { setUserAsOwner: boolean, options: { setUserAsOwner: boolean,
useNewPlan: boolean, useNewPlan: boolean,
planType?: 'free',
externalId?: string, externalId?: string,
externalOptions?: ExternalBillingOptions }, externalOptions?: ExternalBillingOptions },
transaction?: EntityManager): Promise<QueryResult<number>> { transaction?: EntityManager): Promise<QueryResult<number>> {
@ -1110,10 +1113,11 @@ export class HomeDBManager extends EventEmitter {
let billingAccount; let billingAccount;
if (options.useNewPlan) { if (options.useNewPlan) {
const productNames = getDefaultProductNames(); const productNames = getDefaultProductNames();
let productName = options.setUserAsOwner ? productNames.personal : productNames.teamInitial; let productName = options.setUserAsOwner ? productNames.personal :
options.planType === 'free' ? productNames.teamFree : productNames.teamInitial;
// A bit fragile: this is called during creation of support@ user, before // A bit fragile: this is called during creation of support@ user, before
// getSupportUserId() is available, but with setUserAsOwner of true. // getSupportUserId() is available, but with setUserAsOwner of true.
if (!options.setUserAsOwner && user.id === this.getSupportUserId()) { if (!options.setUserAsOwner && user.id === this.getSupportUserId() && options.planType !== 'free') {
// For teams created by support@getgrist.com, set the product to something // For teams created by support@getgrist.com, set the product to something
// good so payment not needed. This is useful for testing. // good so payment not needed. This is useful for testing.
productName = productNames.team; productName = productNames.team;