You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gristlabs_grist-core/app/gen-server/migration/1556726945436-Billing.ts

226 lines
7.3 KiB

import {MigrationInterface, QueryRunner, Table, TableColumn, TableForeignKey} from 'typeorm';
import {BillingAccount} from 'app/gen-server/entity/BillingAccount';
import {BillingAccountManager} from 'app/gen-server/entity/BillingAccountManager';
import {Organization} from 'app/gen-server/entity/Organization';
import {Product} from 'app/gen-server/entity/Product';
import {nativeValues} from 'app/gen-server/lib/values';
export class Billing1556726945436 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
// Create table for products.
await queryRunner.createTable(new Table({
name: 'products',
columns: [
{
name: 'id',
type: 'integer',
isGenerated: true,
generationStrategy: 'increment',
isPrimary: true
},
{
name: 'name',
type: 'varchar'
},
{
name: 'stripe_product_id',
type: 'varchar',
isUnique: true,
isNullable: true
},
{
name: 'features',
type: nativeValues.jsonType
}
]
}));
// Create a basic free product that existing orgs can use.
const product = new Product();
product.name = 'Free';
product.features = {};
await queryRunner.manager.save(product);
// Create billing accounts and billing account managers.
await queryRunner.createTable(new Table({
name: 'billing_accounts',
columns: [
{
name: 'id',
type: 'integer',
isGenerated: true,
generationStrategy: 'increment',
isPrimary: true
},
{
name: 'product_id',
type: 'integer'
},
{
name: 'individual',
type: nativeValues.booleanType
},
{
name: 'in_good_standing',
type: nativeValues.booleanType,
default: nativeValues.trueValue
},
{
name: 'status',
type: nativeValues.jsonType,
isNullable: true
},
{
name: 'stripe_customer_id',
type: 'varchar',
isUnique: true,
isNullable: true
},
{
name: 'stripe_subscription_id',
type: 'varchar',
isUnique: true,
isNullable: true
},
{
name: 'stripe_plan_id',
type: 'varchar',
isNullable: true
}
],
foreignKeys: [
{
columnNames: ['product_id'],
referencedColumnNames: ['id'],
referencedTableName: 'products'
}
]
}));
await queryRunner.createTable(new Table({
name: 'billing_account_managers',
columns: [
{
name: 'id',
type: 'integer',
isGenerated: true,
generationStrategy: 'increment',
isPrimary: true
},
{
name: 'billing_account_id',
type: 'integer'
},
{
name: 'user_id',
type: 'integer'
}
],
foreignKeys: [
{
columnNames: ['billing_account_id'],
referencedColumnNames: ['id'],
referencedTableName: 'billing_accounts',
onDelete: 'CASCADE' // delete manager if referenced billing_account goes away
},
{
columnNames: ['user_id'],
referencedColumnNames: ['id'],
referencedTableName: 'users',
onDelete: 'CASCADE' // delete manager if referenced user goes away
}
]
}));
// Add a reference to billing accounts from orgs.
await queryRunner.addColumn('orgs', new TableColumn({
name: 'billing_account_id',
type: 'integer',
isNullable: true
}));
await queryRunner.createForeignKey('orgs', new TableForeignKey({
columnNames: ['billing_account_id'],
referencedColumnNames: ['id'],
referencedTableName: 'billing_accounts'
}));
// Let's add billing accounts to all existing orgs.
// Personal orgs are put on an individual billing account.
// Other orgs are put on a team billing account, with the
// list of payment managers seeded by owners of that account.
const query =
queryRunner.manager.createQueryBuilder()
.select('orgs.id')
.from(Organization, 'orgs')
.leftJoin('orgs.owner', 'owners')
.addSelect('orgs.owner.id')
.leftJoinAndSelect('orgs.aclRules', 'acl_rules')
.leftJoinAndSelect('acl_rules.group', 'groups')
.leftJoin('groups.memberUsers', 'users')
.addSelect('users.id')
.where('permissions & 8 = 8'); // seed managers with owners+editors, omitting guests+viewers
// (permission 8 is "Remove")
const orgs = await query.getMany();
for (const org of orgs) {
const individual = Boolean(org.owner);
const billingAccountInsert = await queryRunner.manager.createQueryBuilder()
.insert()
.into(BillingAccount)
.values([{product, individual}])
.execute();
const billingAccountId = billingAccountInsert.identifiers[0].id;
if (individual) {
await queryRunner.manager.createQueryBuilder()
.insert()
.into(BillingAccountManager)
.values([{billingAccountId, userId: org.owner.id}])
.execute();
} else {
for (const rule of org.aclRules) {
for (const user of rule.group.memberUsers) {
await queryRunner.manager.createQueryBuilder()
.insert()
.into(BillingAccountManager)
.values([{billingAccountId, userId: user.id}])
.execute();
}
}
}
await queryRunner.manager.createQueryBuilder()
.update(Organization)
.set({billingAccountId})
.where('id = :id', {id: org.id})
.execute();
}
// TODO: in a future migration, orgs.billing_account_id could be constrained
// to be non-null. All code deployments linked to a database that will be
// migrated must have code that sets orgs.billing_account_id by that time,
// otherwise they would fail to create orgs (and remember creating a user
// involves creating an org).
/*
// Now that all orgs have a billing account (and this migration is running within
// a transaction), we can constrain orgs.billing_account_id to be non-null.
const orgTable = (await queryRunner.getTable('orgs'))!;
const billingAccountId = orgTable.findColumnByName('billing_account_id')!;
const billingAccountIdNonNull = billingAccountId.clone();
billingAccountIdNonNull.isNullable = false;
await queryRunner.changeColumn('orgs', billingAccountId, billingAccountIdNonNull);
*/
}
public async down(queryRunner: QueryRunner): Promise<any> {
// this is a bit ugly, but is the documented way to remove a foreign key
const table = await queryRunner.getTable('orgs');
const foreignKey = table!.foreignKeys.find(fk => fk.columnNames.indexOf('billing_account_id') !== -1);
await queryRunner.dropForeignKey('orgs', foreignKey!);
await queryRunner.dropColumn('orgs', 'billing_account_id');
await queryRunner.dropTable('billing_account_managers');
await queryRunner.dropTable('billing_accounts');
await queryRunner.dropTable('products');
}
}