(core) Customizable stripe plans.

Summary:
- Reading plans from Stripe, and allowing Stripe to define custom plans.
- Storing product features (aka limits) in Stripe, that override those in db.
- Adding hierarchical data in Stripe. All features are defined at Product level but can be overwritten on Price levels.
- New options for Support user to
-- Override product for team site (if he is added as a billing manager)
-- Override subscription and customer id for a team site
-- Attach an "offer", an custom plan configured in stripe that a team site can use
-- Enabling wire transfer for subscription by allowing subscription to be created without a payment method (which is customizable)

Test Plan: Updated and new.

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D4201
This commit is contained in:
Jarosław Sadziński
2024-05-17 21:14:34 +02:00
parent ed9514bae0
commit 60423edc17
40 changed files with 720 additions and 248 deletions

View File

@@ -10,6 +10,7 @@ import {Organization} from 'app/gen-server/entity/Organization';
import {Product} from 'app/gen-server/entity/Product';
import {HomeDBManager, UserChange} from 'app/gen-server/lib/HomeDBManager';
import {TestServer} from 'test/gen-server/apiUtils';
import {TEAM_FREE_PLAN} from 'app/common/Features';
const assert = chai.assert;
@@ -920,6 +921,9 @@ describe('ApiServer', function() {
status: null,
externalId: null,
externalOptions: null,
features: null,
stripePlanId: null,
paymentLink: null,
},
});
assert.isNotNull(org.updatedAt);
@@ -2151,7 +2155,7 @@ describe('ApiServer', function() {
'best-friends-squad', false);
await dbManager.connection.query(
'update billing_accounts set product_id = (select id from products where name = $1) where id = $2',
['teamFree', prevAccount.id]
[TEAM_FREE_PLAN, prevAccount.id]
);
const resp = await axios.post(`${homeUrl}/api/orgs/${freeTeamOrgId}/workspaces`, {

View File

@@ -64,8 +64,8 @@ describe('ApiSession', function() {
'createdAt', 'updatedAt', 'host']);
assert.deepEqual(resp.data.org.billingAccount,
{ id: 1, individual: false, inGoodStanding: true, status: null,
externalId: null, externalOptions: null,
isManager: true, paid: false,
externalId: null, externalOptions: null, paymentLink: null,
isManager: true, paid: false, features: null, stripePlanId: null,
product: { id: 1, name: 'Free', features: {workspaces: true, vanityDomain: true} } });
// Check that internally we have access to stripe ids.
@@ -74,7 +74,7 @@ describe('ApiSession', function() {
assert.hasAllKeys(org2.data!.billingAccount,
['id', 'individual', 'inGoodStanding', 'status', 'stripeCustomerId',
'stripeSubscriptionId', 'stripePlanId', 'product', 'paid', 'isManager',
'externalId', 'externalOptions']);
'externalId', 'externalOptions', 'features', 'paymentLink']);
});
it('GET /api/session/access/active returns orgErr when org is forbidden', async function() {

View File

@@ -41,6 +41,7 @@ import {ForkIndexes1678737195050 as ForkIndexes} from 'app/gen-server/migration/
import {ActivationPrefs1682636695021 as ActivationPrefs} from 'app/gen-server/migration/1682636695021-ActivationPrefs';
import {AssistantLimit1685343047786 as AssistantLimit} from 'app/gen-server/migration/1685343047786-AssistantLimit';
import {Shares1701557445716 as Shares} from 'app/gen-server/migration/1701557445716-Shares';
import {Billing1711557445716 as BillingFeatures} from 'app/gen-server/migration/1711557445716-Billing';
const home: HomeDBManager = new HomeDBManager();
@@ -49,7 +50,7 @@ const migrations = [Initial, Login, PinDocs, UserPicture, DisplayEmail, DisplayE
CustomerIndex, ExtraIndexes, OrgHost, DocRemovedAt, Prefs,
ExternalBilling, DocOptions, Secret, UserOptions, GracePeriodStart,
DocumentUsage, Activations, UserConnectId, UserUUID, UserUniqueRefUUID,
Forks, ForkIndexes, ActivationPrefs, AssistantLimit, Shares];
Forks, ForkIndexes, ActivationPrefs, AssistantLimit, Shares, BillingFeatures];
// Assert that the "members" acl rule and group exist (or not).
function assertMembersGroup(org: Organization, exists: boolean) {

View File

@@ -37,7 +37,7 @@ import {Document} from "app/gen-server/entity/Document";
import {Group} from "app/gen-server/entity/Group";
import {Login} from "app/gen-server/entity/Login";
import {Organization} from "app/gen-server/entity/Organization";
import {Product, PRODUCTS, synchronizeProducts, testDailyApiLimitFeatures} from "app/gen-server/entity/Product";
import {Product, PRODUCTS, synchronizeProducts, teamFreeFeatures} from "app/gen-server/entity/Product";
import {User} from "app/gen-server/entity/User";
import {Workspace} from "app/gen-server/entity/Workspace";
import {EXAMPLE_WORKSPACE_NAME} from 'app/gen-server/lib/HomeDBManager';
@@ -48,6 +48,13 @@ import * as fse from 'fs-extra';
const ACCESS_GROUPS = ['owners', 'editors', 'viewers', 'guests', 'members'];
export const testDailyApiLimitFeatures = {
...teamFreeFeatures,
baseMaxApiUnitsPerDocumentPerDay: 3,
};
const testProducts = [
...PRODUCTS,
{
@@ -428,7 +435,11 @@ class Seed {
const ba = new BillingAccount();
ba.individual = false;
const productName = org.product || 'Free';
ba.product = (await Product.findOne({where: {name: productName}}))!;
const product = await Product.findOne({where: {name: productName}});
if (!product) {
throw new Error(`Product not found: ${productName}`);
}
ba.product = product;
o.billingAccount = ba;
if (org.domain) { o.domain = org.domain; }
if (org.host) { o.host = org.host; }