(core) Billing for formula assistant

Summary:
Adding limits for AI calls and connecting those limits with a Stripe Account.

- New table in homedb called `limits`
- All calls to the AI are not routed through DocApi and measured.
- All products now contain a special key `assistantLimit`, with a default value 0
- Limit is reset every time the subscription has changed its period
- The billing page is updated with two new options that describe the AI plan
- There is a new popup that allows the user to upgrade to a higher plan
- Tiers are read directly from the Stripe product with a volume pricing model

Test Plan: Updated and added

Reviewers: georgegevoian, paulfitz

Reviewed By: georgegevoian

Subscribers: dsagal

Differential Revision: https://phab.getgrist.com/D3907
This commit is contained in:
Jarosław Sadziński
2023-07-05 17:36:45 +02:00
parent 75d979abdb
commit d13b9b9019
26 changed files with 501 additions and 106 deletions

View File

@@ -558,7 +558,7 @@ describe('DocTutorial', function () {
// Check that the update is immediately reflected in the tutorial popup.
assert.equal(
await driver.find('.test-doc-tutorial-popup p').getText(),
await driver.findWait('.test-doc-tutorial-popup p', 2000).getText(),
'Welcome to the Grist Basics tutorial V2.'
);
@@ -571,7 +571,7 @@ describe('DocTutorial', function () {
// Switch to another user and restart the tutorial.
viewerSession = await gu.session().teamSite.user('user2').login();
await viewerSession.loadDoc(`/doc/${doc.id}`);
await driver.find('.test-doc-tutorial-popup-restart').click();
await driver.findWait('.test-doc-tutorial-popup-restart', 2000).click();
await driver.find('.test-modal-confirm').click();
await gu.waitForServer();
await driver.findWait('.test-doc-tutorial-popup', 2000);

View File

@@ -2056,7 +2056,13 @@ export class Session {
isFirstLogin?: boolean,
showTips?: boolean,
skipTutorial?: boolean, // By default true
userName?: string,
email?: string,
retainExistingLogin?: boolean}) {
if (options?.userName) {
this.settings.name = options.userName;
this.settings.email = options.email || '';
}
// Optimize testing a little bit, so if we are already logged in as the expected
// user on the expected org, and there are no options set, we can just continue.
if (!options && await this.isLoggedInCorrectly()) { return this; }
@@ -3150,20 +3156,26 @@ export async function availableBehaviorOptions() {
return list;
}
export function withComments() {
let oldEnv: testUtils.EnvironmentSnapshot;
/**
* Restarts the server ensuring that it is run with the given environment variables.
* If variables are already set, the server is not restarted.
*
* Useful for local testing of features that depend on environment variables, as it avoids the need
* to restart the server when those variables are already set.
*/
export function withEnvironmentSnapshot(vars: Record<string, any>) {
let oldEnv: testUtils.EnvironmentSnapshot|null = null;
before(async () => {
if (process.env.COMMENTS !== 'true') {
oldEnv = new testUtils.EnvironmentSnapshot();
process.env.COMMENTS = 'true';
await server.restart();
}
// Test if the vars are already set, and if so, skip.
if (Object.keys(vars).every(k => process.env[k] === vars[k])) { return; }
oldEnv = new testUtils.EnvironmentSnapshot();
Object.assign(process.env, vars);
await server.restart();
});
after(async () => {
if (oldEnv) {
oldEnv.restore();
await server.restart();
}
if (!oldEnv) { return; }
oldEnv.restore();
await server.restart();
});
}