(core) Removing temporary pro site

Summary: Creating a pro team site after Stripe checkout. Previously a stub site was always created and never removed, even if a user cancels the checkout process, which resulted in multiple 'ghost' sites that can't be removed.

Test Plan: Updated and added

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3985
This commit is contained in:
Jarosław Sadziński 2023-08-09 10:35:44 +02:00
parent f1a0b61e15
commit fad421b7c0
3 changed files with 44 additions and 16 deletions

View File

@ -229,7 +229,8 @@ export class BillingAPIImpl extends BaseAPI implements BillingAPI {
body: JSON.stringify({ body: JSON.stringify({
domain, domain,
name, name,
planType: 'team' planType: 'team',
next: window.location.href
}) })
}); });
return data.checkoutUrl; return data.checkoutUrl;

View File

@ -7,6 +7,8 @@ import {Request} from 'express';
import {ApiError} from 'app/common/ApiError'; import {ApiError} from 'app/common/ApiError';
import {FullUser} from 'app/common/LoginSessionAPI'; import {FullUser} from 'app/common/LoginSessionAPI';
import {OrganizationProperties} from 'app/common/UserAPI'; import {OrganizationProperties} from 'app/common/UserAPI';
import {User} from 'app/gen-server/entity/User';
import {BillingOptions, HomeDBManager, QueryResult, Scope} from 'app/gen-server/lib/HomeDBManager';
import {getAuthorizedUserId, getUserId, getUserProfiles, RequestWithLogin} from 'app/server/lib/Authorizer'; import {getAuthorizedUserId, getUserId, getUserProfiles, RequestWithLogin} from 'app/server/lib/Authorizer';
import {getSessionUser, linkOrgWithEmail} from 'app/server/lib/BrowserSession'; import {getSessionUser, linkOrgWithEmail} from 'app/server/lib/BrowserSession';
import {expressWrap} from 'app/server/lib/expressWrap'; import {expressWrap} from 'app/server/lib/expressWrap';
@ -16,9 +18,6 @@ import log from 'app/server/lib/log';
import {addPermit, clearSessionCacheIfNeeded, getDocScope, getScope, integerParam, import {addPermit, clearSessionCacheIfNeeded, getDocScope, getScope, integerParam,
isParameterOn, optStringParam, sendOkReply, sendReply, stringParam} from 'app/server/lib/requestUtils'; isParameterOn, optStringParam, sendOkReply, sendReply, stringParam} from 'app/server/lib/requestUtils';
import {IWidgetRepository} from 'app/server/lib/WidgetRepository'; import {IWidgetRepository} from 'app/server/lib/WidgetRepository';
import {User} from './entity/User';
import {HomeDBManager, QueryResult, Scope} from './lib/HomeDBManager';
import {getCookieDomain} from 'app/server/lib/gristSessions'; import {getCookieDomain} from 'app/server/lib/gristSessions';
// exposed for testing purposes // exposed for testing purposes
@ -64,7 +63,7 @@ export function getOrgKey(req: Request): string|number {
return orgKey; return orgKey;
} }
// Adds an non-personal org with a new billingAccout, with the given name and domain. // Adds an non-personal org with a new billingAccount, with the given name and domain.
// Returns a QueryResult with the orgId on success. // Returns a QueryResult with the orgId on success.
export function addOrg( export function addOrg(
dbManager: HomeDBManager, dbManager: HomeDBManager,
@ -72,6 +71,7 @@ export function addOrg(
props: Partial<OrganizationProperties>, props: Partial<OrganizationProperties>,
options?: { options?: {
planType?: string, planType?: string,
billing?: BillingOptions,
} }
): Promise<number> { ): Promise<number> {
return dbManager.connection.transaction(async manager => { return dbManager.connection.transaction(async manager => {

View File

@ -27,7 +27,7 @@ import {
} from "app/common/UserAPI"; } from "app/common/UserAPI";
import {AclRule, AclRuleDoc, AclRuleOrg, AclRuleWs} from "app/gen-server/entity/AclRule"; import {AclRule, AclRuleDoc, AclRuleOrg, AclRuleWs} from "app/gen-server/entity/AclRule";
import {Alias} from "app/gen-server/entity/Alias"; import {Alias} from "app/gen-server/entity/Alias";
import {BillingAccount, ExternalBillingOptions} from "app/gen-server/entity/BillingAccount"; import {BillingAccount} from "app/gen-server/entity/BillingAccount";
import {BillingAccountManager} from "app/gen-server/entity/BillingAccountManager"; import {BillingAccountManager} from "app/gen-server/entity/BillingAccountManager";
import {Document} from "app/gen-server/entity/Document"; import {Document} from "app/gen-server/entity/Document";
import {Group} from "app/gen-server/entity/Group"; import {Group} from "app/gen-server/entity/Group";
@ -260,6 +260,20 @@ interface CreateWorkspaceOptions {
ownerId?: number ownerId?: number
} }
/**
* Available options for creating a new org with a new billing account.
*/
export type BillingOptions = Partial<Pick<BillingAccount,
'product' |
'stripeCustomerId' |
'stripeSubscriptionId' |
'stripePlanId' |
'externalId' |
'externalOptions' |
'inGoodStanding' |
'status'
>>;
/** /**
* HomeDBManager handles interaction between the ApiServer and the Home database, * HomeDBManager handles interaction between the ApiServer and the Home database,
* encapsulating the typeorm logic. * encapsulating the typeorm logic.
@ -1348,14 +1362,13 @@ export class HomeDBManager extends EventEmitter {
* NOTE: Currently it is always a true - billing account is one to one with org. * NOTE: Currently it is always a true - billing account is one to one with org.
* @param planType: if set, controls the type of plan used for the org. Only * @param planType: if set, controls the type of plan used for the org. Only
* meaningful for team sites currently. * meaningful for team sites currently.
* * @param billing: if set, controls the billing account settings for the org.
*/ */
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?: string, planType?: string,
externalId?: string, billing?: BillingOptions},
externalOptions?: ExternalBillingOptions },
transaction?: EntityManager): Promise<QueryResult<number>> { transaction?: EntityManager): Promise<QueryResult<number>> {
const notifications: Array<() => void> = []; const notifications: Array<() => void> = [];
const name = props.name; const name = props.name;
@ -1405,12 +1418,26 @@ export class HomeDBManager extends EventEmitter {
billingAccountManager.user = user; billingAccountManager.user = user;
billingAccountManager.billingAccount = billingAccount; billingAccountManager.billingAccount = billingAccount;
billingAccountEntities.push(billingAccountManager); billingAccountEntities.push(billingAccountManager);
if (options.externalId) { // Apply billing settings if requested, but not all of them.
// save will fail if externalId is a duplicate. if (options.billing) {
billingAccount.externalId = options.externalId; const billing = options.billing;
} const allowedKeys: Array<keyof BillingOptions> = [
if (options.externalOptions) { 'product',
billingAccount.externalOptions = options.externalOptions; 'stripeCustomerId',
'stripeSubscriptionId',
'stripePlanId',
// save will fail if externalId is a duplicate.
'externalId',
'externalOptions',
'inGoodStanding',
'status'
];
Object.keys(billing).forEach(key => {
if (!allowedKeys.includes(key as any)) {
delete (billing as any)[key];
}
});
Object.assign(billingAccount, billing);
} }
} else { } else {
log.warn("Creating org with shared billing account"); log.warn("Creating org with shared billing account");
@ -1421,7 +1448,7 @@ export class HomeDBManager extends EventEmitter {
.leftJoinAndSelect('billing_accounts.orgs', 'orgs') .leftJoinAndSelect('billing_accounts.orgs', 'orgs')
.where('orgs.owner_id = :userId', {userId: user.id}) .where('orgs.owner_id = :userId', {userId: user.id})
.getOne(); .getOne();
if (options.externalId && billingAccount?.externalId !== options.externalId) { if (options.billing?.externalId && billingAccount?.externalId !== options.billing?.externalId) {
throw new ApiError('Conflicting external identifier', 400); throw new ApiError('Conflicting external identifier', 400);
} }
if (!billingAccount) { if (!billingAccount) {