(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({
domain,
name,
planType: 'team'
planType: 'team',
next: window.location.href
})
});
return data.checkoutUrl;

View File

@ -7,6 +7,8 @@ import {Request} from 'express';
import {ApiError} from 'app/common/ApiError';
import {FullUser} from 'app/common/LoginSessionAPI';
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 {getSessionUser, linkOrgWithEmail} from 'app/server/lib/BrowserSession';
import {expressWrap} from 'app/server/lib/expressWrap';
@ -16,9 +18,6 @@ import log from 'app/server/lib/log';
import {addPermit, clearSessionCacheIfNeeded, getDocScope, getScope, integerParam,
isParameterOn, optStringParam, sendOkReply, sendReply, stringParam} from 'app/server/lib/requestUtils';
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';
// exposed for testing purposes
@ -64,7 +63,7 @@ export function getOrgKey(req: Request): string|number {
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.
export function addOrg(
dbManager: HomeDBManager,
@ -72,6 +71,7 @@ export function addOrg(
props: Partial<OrganizationProperties>,
options?: {
planType?: string,
billing?: BillingOptions,
}
): Promise<number> {
return dbManager.connection.transaction(async manager => {

View File

@ -27,7 +27,7 @@ import {
} from "app/common/UserAPI";
import {AclRule, AclRuleDoc, AclRuleOrg, AclRuleWs} from "app/gen-server/entity/AclRule";
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 {Document} from "app/gen-server/entity/Document";
import {Group} from "app/gen-server/entity/Group";
@ -260,6 +260,20 @@ interface CreateWorkspaceOptions {
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,
* 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.
* @param planType: if set, controls the type of plan used for the org. Only
* 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>,
options: { setUserAsOwner: boolean,
useNewPlan: boolean,
planType?: string,
externalId?: string,
externalOptions?: ExternalBillingOptions },
billing?: BillingOptions},
transaction?: EntityManager): Promise<QueryResult<number>> {
const notifications: Array<() => void> = [];
const name = props.name;
@ -1405,12 +1418,26 @@ export class HomeDBManager extends EventEmitter {
billingAccountManager.user = user;
billingAccountManager.billingAccount = billingAccount;
billingAccountEntities.push(billingAccountManager);
if (options.externalId) {
// save will fail if externalId is a duplicate.
billingAccount.externalId = options.externalId;
}
if (options.externalOptions) {
billingAccount.externalOptions = options.externalOptions;
// Apply billing settings if requested, but not all of them.
if (options.billing) {
const billing = options.billing;
const allowedKeys: Array<keyof BillingOptions> = [
'product',
'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 {
log.warn("Creating org with shared billing account");
@ -1421,7 +1448,7 @@ export class HomeDBManager extends EventEmitter {
.leftJoinAndSelect('billing_accounts.orgs', 'orgs')
.where('orgs.owner_id = :userId', {userId: user.id})
.getOne();
if (options.externalId && billingAccount?.externalId !== options.externalId) {
if (options.billing?.externalId && billingAccount?.externalId !== options.billing?.externalId) {
throw new ApiError('Conflicting external identifier', 400);
}
if (!billingAccount) {