mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-20 17:14:11 +00:00
177 lines
4.8 KiB
TypeScript
177 lines
4.8 KiB
TypeScript
|
import {Features} from 'app/common/Features';
|
||
|
import {nativeValues} from 'app/gen-server/lib/values';
|
||
|
import * as assert from 'assert';
|
||
|
import {BaseEntity, Column, Connection, Entity, PrimaryGeneratedColumn} from 'typeorm';
|
||
|
|
||
|
/**
|
||
|
* A summary of features used in 'starter' plans.
|
||
|
*/
|
||
|
export const starterFeatures: Features = {
|
||
|
workspaces: true,
|
||
|
// no vanity domain
|
||
|
maxDocsPerOrg: 10,
|
||
|
maxSharesPerDoc: 2,
|
||
|
maxWorkspacesPerOrg: 1
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* A summary of features used in 'team' plans.
|
||
|
*/
|
||
|
export const teamFeatures: Features = {
|
||
|
workspaces: true,
|
||
|
vanityDomain: true,
|
||
|
maxSharesPerWorkspace: 0, // all workspace shares need to be org members.
|
||
|
maxSharesPerDoc: 2
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* A summary of features used in unrestricted grandfathered accounts, and also
|
||
|
* in some test settings.
|
||
|
*/
|
||
|
export const grandfatherFeatures: Features = {
|
||
|
workspaces: true,
|
||
|
vanityDomain: true,
|
||
|
};
|
||
|
|
||
|
export const suspendedFeatures: Features = {
|
||
|
workspaces: true,
|
||
|
vanityDomain: true,
|
||
|
readOnlyDocs: true,
|
||
|
// clamp down on new docs/workspaces/shares
|
||
|
maxDocsPerOrg: 0,
|
||
|
maxSharesPerDoc: 0,
|
||
|
maxWorkspacesPerOrg: 0,
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Basic fields needed for products supported by Grist.
|
||
|
*/
|
||
|
export interface IProduct {
|
||
|
name: string;
|
||
|
features: Features;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* Products are a bundle of enabled features. Most products in
|
||
|
* Grist correspond to products in stripe. The correspondence is
|
||
|
* established by a gristProduct metadata field on stripe plans.
|
||
|
*
|
||
|
* In addition, there are the following products in Grist that don't
|
||
|
* exist in stripe:
|
||
|
* - The product named 'Free'. This is a product used for organizations
|
||
|
* created prior to the billing system being set up.
|
||
|
* - The product named 'stub'. This is product assigned to new
|
||
|
* organizations that should not be usable until a paid plan
|
||
|
* is set up for them.
|
||
|
*
|
||
|
* TODO: change capitalization of name of grandfather product.
|
||
|
*
|
||
|
*/
|
||
|
const PRODUCTS: IProduct[] = [
|
||
|
// This is a product for grandfathered accounts/orgs.
|
||
|
{
|
||
|
name: 'Free',
|
||
|
features: grandfatherFeatures,
|
||
|
},
|
||
|
|
||
|
// This is a product for newly created accounts/orgs.
|
||
|
{
|
||
|
name: 'stub',
|
||
|
features: {},
|
||
|
},
|
||
|
|
||
|
// These are products set up in stripe.
|
||
|
{
|
||
|
name: 'starter',
|
||
|
features: starterFeatures,
|
||
|
},
|
||
|
{
|
||
|
name: 'professional', // deprecated, can be removed once no longer referred to in stripe.
|
||
|
features: teamFeatures,
|
||
|
},
|
||
|
{
|
||
|
name: 'team',
|
||
|
features: teamFeatures,
|
||
|
},
|
||
|
|
||
|
// This is a product for a team site that is no longer in good standing, but isn't yet
|
||
|
// to be removed / deactivated entirely.
|
||
|
{
|
||
|
name: 'suspended',
|
||
|
features: suspendedFeatures,
|
||
|
},
|
||
|
];
|
||
|
|
||
|
/**
|
||
|
* Get names of products for different situations.
|
||
|
*/
|
||
|
export function getDefaultProductNames() {
|
||
|
return {
|
||
|
personal: 'starter', // Personal site start off on a functional plan.
|
||
|
teamInitial: 'stub', // Team site starts off on a limited plan, requiring subscription.
|
||
|
team: 'team', // Functional team site
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A Grist product. Corresponds to a set of enabled features and a choice of limits.
|
||
|
*/
|
||
|
@Entity({name: 'products'})
|
||
|
export class Product extends BaseEntity {
|
||
|
@PrimaryGeneratedColumn()
|
||
|
public id: number;
|
||
|
|
||
|
@Column()
|
||
|
public name: string;
|
||
|
|
||
|
@Column({type: nativeValues.jsonEntityType})
|
||
|
public features: Features;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Make sure the products defined for the current stripe setup are
|
||
|
* in the database and up to date. Other products in the database
|
||
|
* are untouched.
|
||
|
*
|
||
|
* If `apply` is set, the products are changed in the db, otherwise
|
||
|
* the are left unchanged. A summary of affected products is returned.
|
||
|
*/
|
||
|
export async function synchronizeProducts(connection: Connection, apply: boolean): Promise<string[]> {
|
||
|
try {
|
||
|
await connection.query('select name, features, stripe_product_id from products limit 1');
|
||
|
} catch (e) {
|
||
|
// No usable products table, do not try to synchronize.
|
||
|
return [];
|
||
|
}
|
||
|
const changingProducts: string[] = [];
|
||
|
await connection.transaction(async transaction => {
|
||
|
const desiredProducts = new Map(PRODUCTS.map(p => [p.name, p]));
|
||
|
const existingProducts = new Map((await transaction.find(Product))
|
||
|
.map(p => [p.name, p]));
|
||
|
for (const product of desiredProducts.values()) {
|
||
|
if (existingProducts.has(product.name)) {
|
||
|
const p = existingProducts.get(product.name)!;
|
||
|
try {
|
||
|
assert.deepStrictEqual(p.features, product.features);
|
||
|
} catch (e) {
|
||
|
if (apply) {
|
||
|
p.features = product.features;
|
||
|
await transaction.save(p);
|
||
|
}
|
||
|
changingProducts.push(p.name);
|
||
|
}
|
||
|
} else {
|
||
|
if (apply) {
|
||
|
const p = new Product();
|
||
|
p.name = product.name;
|
||
|
p.features = product.features;
|
||
|
await transaction.save(p);
|
||
|
}
|
||
|
changingProducts.push(product.name);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
return changingProducts;
|
||
|
}
|