(core) add users.options.isConsultant flag, and omit such users from billing

Summary:
This adds an optional `isConsultant` flag to `users.options`, and an endpoint that allows the support user to turn it on or off. Users marked as consultants are not counted as billable members. Follows the example of existing `allowGoogleLogin` option.

Billable members are counted when members are added or removed from a site. Changing the `isConsultant` flag has no immediate or retroactive effect on billing. The number of users in stripe is now set unconditionally, rather than only when it has changed.

Notifications to billing managers are not aware of this billing nuance, but continue to report user counts that include consultants. The notifications link users to the billing page.

Test Plan: extended test

Reviewers: georgegevoian

Reviewed By: georgegevoian

Subscribers: anaisconce, jarek

Differential Revision: https://phab.getgrist.com/D3362
This commit is contained in:
Paul Fitzpatrick 2022-04-08 09:52:08 -04:00
parent 782bb44ed5
commit 14f7e30e6f
4 changed files with 32 additions and 2 deletions

View File

@ -174,7 +174,7 @@ export class BillingPage extends Disposable {
] : null,
moneyPlan.amount ? [
makeSummaryFeature([`Your team site has `, `${sub.userCount}`,
` member${sub.userCount > 1 ? 's' : ''}`]),
` member${sub.userCount !== 1 ? 's' : ''}`]),
tier ? this.buildAppSumoPlanNotes(discountName!) : null,
// Currently the subtotal is misleading and scary when tiers are in effect.
// In this case, for now, just report what will be invoiced.

View File

@ -135,6 +135,9 @@ export interface Document extends DocumentProperties {
export interface UserOptions {
// Whether signing in with Google is allowed. Defaults to true if unset.
allowGoogleLogin?: boolean;
// Whether user is a consultant. Consultant users can be added to sites
// without being counted for billing. Defaults to false if unset.
isConsultant?: boolean;
}
export interface PermissionDelta {
@ -312,6 +315,7 @@ export interface UserAPI {
getUserProfile(): Promise<FullUser>;
updateUserName(name: string): Promise<void>;
updateAllowGoogleLogin(allowGoogleLogin: boolean): Promise<void>;
updateIsConsultant(userId: number, isConsultant: boolean): Promise<void>;
getWorker(key: string): Promise<string>;
getWorkerAPI(key: string): Promise<DocWorkerAPI>;
getBillingAPI(): BillingAPI;
@ -608,6 +612,13 @@ export class UserAPIImpl extends BaseAPI implements UserAPI {
});
}
public async updateIsConsultant(userId: number, isConsultant: boolean): Promise<void> {
await this.request(`${this._url}/api/profile/isConsultant`, {
method: 'POST',
body: JSON.stringify({userId, isConsultant})
});
}
public async getWorker(key: string): Promise<string> {
const json = await this.requestJson(`${this._url}/api/worker/${key}`, {
method: 'GET',

View File

@ -379,6 +379,25 @@ export class ApiServer {
res.sendStatus(200);
}));
this._app.post('/api/profile/isConsultant', expressWrap(async (req, res) => {
const userId = getAuthorizedUserId(req);
if (userId !== this._dbManager.getSupportUserId()) {
throw new ApiError('Only support user can enable/disable isConsultant', 401);
}
const isConsultant: boolean | undefined = req.body.isConsultant;
const targetUserId: number | undefined = req.body.userId;
if (isConsultant === undefined) {
throw new ApiError('Missing body param: isConsultant', 400);
}
if (targetUserId === undefined) {
throw new ApiError('Missing body param: targetUserId', 400);
}
await this._dbManager.updateUserOptions(targetUserId, {
isConsultant
});
res.sendStatus(200);
}));
// GET /api/profile/apikey
// Get user's apiKey
this._app.get('/api/profile/apikey', expressWrap(async (req, res) => {

View File

@ -4185,7 +4185,7 @@ function getFrom(queryBuilder: SelectQueryBuilder<any>): string {
}
// Flatten a map of users per role into a simple list of users.
function removeRole(usersWithRoles: Map<roles.NonGuestRole, User[]>) {
export function removeRole(usersWithRoles: Map<roles.NonGuestRole, User[]>) {
return flatten([...usersWithRoles.values()]);
}