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 { // 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 { // 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'); } }