(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

@@ -176,7 +176,7 @@ export function addSiteCommand(program: commander.Command,
}, {
setUserAsOwner: false,
useNewPlan: true,
planType: 'teamFree'
product: 'teamFree'
}));
});
}

View File

@@ -1893,7 +1893,7 @@ export class DocWorkerApi {
// or to be wrongly rejected after upgrading.
const doc = (req as RequestWithLogin).docAuth!.cachedDoc!;
const max = doc.workspace.org.billingAccount?.product.features.baseMaxApiUnitsPerDocumentPerDay;
const max = doc.workspace.org.billingAccount?.getFeatures().baseMaxApiUnitsPerDocumentPerDay;
if (!max) {
// This doc has no associated product (happens to new unsaved docs)
// or the product has no API limit. Allow the request through.

View File

@@ -1092,7 +1092,7 @@ export class FlexServer implements GristServer {
// If "welcomeNewUser" is ever added to billing pages, we'd need
// to avoid a redirect loop.
if (orgInfo.billingAccount.isManager && orgInfo.billingAccount.product.features.vanityDomain) {
if (orgInfo.billingAccount.isManager && orgInfo.billingAccount.getFeatures().vanityDomain) {
const prefix = isOrgInPathOnly(req.hostname) ? `/o/${mreq.org}` : '';
return res.redirect(`${prefix}/billing/payment?billingTask=signUpLite`);
}

View File

@@ -175,8 +175,8 @@ export class HostedStorageManager implements IDocStorageManager {
return path.join(dir, 'meta.json');
},
async docId => {
const product = await dbManager.getDocProduct(docId);
return product?.features.snapshotWindow;
const features = await dbManager.getDocFeatures(docId);
return features?.snapshotWindow;
},
);

View File

@@ -22,7 +22,7 @@ export const TEST_HTTPS_OFFSET = process.env.GRIST_TEST_HTTPS_OFFSET ?
// Database fields that we permit in entities but don't want to cross the api.
const INTERNAL_FIELDS = new Set([
'apiKey', 'billingAccountId', 'firstLoginAt', 'filteredOut', 'ownerId', 'gracePeriodStart', 'stripeCustomerId',
'stripeSubscriptionId', 'stripePlanId', 'stripeProductId', 'userId', 'isFirstTimeUser', 'allowGoogleLogin',
'stripeSubscriptionId', 'stripeProductId', 'userId', 'isFirstTimeUser', 'allowGoogleLogin',
'authSubject', 'usage', 'createdBy'
]);