diff --git a/app/common/Features.ts b/app/common/Features.ts index a508ea81..9c0cadf6 100644 --- a/app/common/Features.ts +++ b/app/common/Features.ts @@ -34,6 +34,18 @@ export interface Features { // (default: unlimited) 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 diff --git a/app/gen-server/ApiServer.ts b/app/gen-server/ApiServer.ts index a47e1744..c5d1ea64 100644 --- a/app/gen-server/ApiServer.ts +++ b/app/gen-server/ApiServer.ts @@ -72,11 +72,15 @@ export function addOrg( dbManager: HomeDBManager, userId: number, props: Partial, + options?: { + planType?: 'free' + } ): Promise { return dbManager.connection.transaction(async manager => { const user = await manager.findOne(User, userId); if (!user) { return handleDeletedUser(); } const query = await dbManager.addOrg(user, props, { + ...options, setUserAsOwner: false, useNewPlan: true }, manager); diff --git a/app/gen-server/entity/Product.ts b/app/gen-server/entity/Product.ts index 320b5ed5..b8ab5a92 100644 --- a/app/gen-server/entity/Product.ts +++ b/app/gen-server/entity/Product.ts @@ -24,6 +24,21 @@ export const teamFeatures: Features = { 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 * in some test settings. @@ -101,6 +116,10 @@ const PRODUCTS: IProduct[] = [ name: 'suspended', features: suspendedFeatures, }, + { + name: 'teamFree', + features: teamFreeFeatures, + }, ]; /** @@ -112,7 +131,8 @@ export function getDefaultProductNames() { teamInitial: 'stub', // Team site starts off on a limited plan, requiring subscription. teamCancel: 'suspended', // Team site that has been 'turned off'. team: 'team', // Functional team site. - }; + teamFree: 'teamFree', + }; } /** diff --git a/app/gen-server/lib/HomeDBManager.ts b/app/gen-server/lib/HomeDBManager.ts index 4c8f56eb..dece65aa 100644 --- a/app/gen-server/lib/HomeDBManager.ts +++ b/app/gen-server/lib/HomeDBManager.ts @@ -1077,11 +1077,14 @@ export class HomeDBManager extends EventEmitter { * @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 * 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, options: { setUserAsOwner: boolean, useNewPlan: boolean, + planType?: 'free', externalId?: string, externalOptions?: ExternalBillingOptions }, transaction?: EntityManager): Promise> { @@ -1110,10 +1113,11 @@ export class HomeDBManager extends EventEmitter { let billingAccount; if (options.useNewPlan) { 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 // 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 // good so payment not needed. This is useful for testing. productName = productNames.team;