Skip to content

Commit 8d35e44

Browse files
committed
[server] Integrate BillingService
1 parent b50bc8e commit 8d35e44

File tree

2 files changed

+33
-73
lines changed

2 files changed

+33
-73
lines changed

components/server/ee/src/billing/entitlement-service-ubp.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
WORKSPACE_TIMEOUT_DEFAULT_LONG,
1313
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
1414
} from "@gitpod/gitpod-protocol";
15+
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
1516
import { inject, injectable } from "inversify";
1617
import {
1718
EntitlementService,
@@ -20,6 +21,7 @@ import {
2021
} from "../../../src/billing/entitlement-service";
2122
import { Config } from "../../../src/config";
2223
import { BillingModes } from "./billing-mode";
24+
import { BillingService } from "./billing-service";
2325

2426
const MAX_PARALLEL_WORKSPACES_FREE = 4;
2527
const MAX_PARALLEL_WORKSPACES_PAID = 16;
@@ -32,6 +34,7 @@ export class EntitlementServiceUBP implements EntitlementService {
3234
@inject(Config) protected readonly config: Config;
3335
@inject(UserDB) protected readonly userDb: UserDB;
3436
@inject(BillingModes) protected readonly billingModes: BillingModes;
37+
@inject(BillingService) protected readonly billingService: BillingService;
3538

3639
async mayStartWorkspace(
3740
user: User,
@@ -50,19 +53,23 @@ export class EntitlementServiceUBP implements EntitlementService {
5053
return undefined;
5154
}
5255
};
53-
const [spendingLimitReached, hitParallelWorkspaceLimit] = await Promise.all([
54-
this.checkSpendingLimitReached(user.id, date),
56+
const [spendingLimitReachedOnCostCenter, hitParallelWorkspaceLimit] = await Promise.all([
57+
this.checkSpendingLimitReached(user, date),
5558
hasHitParallelWorkspaceLimit(),
5659
]);
5760

5861
return {
59-
spendingLimitReached,
62+
spendingLimitReachedOnCostCenter,
6063
hitParallelWorkspaceLimit,
6164
};
6265
}
6366

64-
protected async checkSpendingLimitReached(userId: string, date: Date): Promise<boolean> {
65-
return false;
67+
protected async checkSpendingLimitReached(user: User, date: Date): Promise<AttributionId | undefined> {
68+
const result = await this.billingService.checkSpendingLimitReached(user);
69+
if (result.reached) {
70+
return result.attributionId;
71+
}
72+
return undefined;
6673
}
6774

6875
protected async getMaxParallelWorkspaces(user: User, date: Date): Promise<number> {

components/server/ee/src/workspace/gitpod-server-impl.ts

Lines changed: 21 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ import {
4646
FindPrebuildsParams,
4747
TeamMemberRole,
4848
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
49-
WorkspaceType,
5049
PrebuildEvent,
5150
} from "@gitpod/gitpod-protocol";
5251
import { ResponseError } from "vscode-jsonrpc";
@@ -72,7 +71,7 @@ import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositor
7271
import { EligibilityService } from "../user/eligibility-service";
7372
import { AccountStatementProvider } from "../user/account-statement-provider";
7473
import { GithubUpgradeURL, PlanCoupon } from "@gitpod/gitpod-protocol/lib/payment-protocol";
75-
import { BillableSession, BillableSessionRequest, SortOrder } from "@gitpod/gitpod-protocol/lib/usage";
74+
import { BillableSession, BillableSessionRequest } from "@gitpod/gitpod-protocol/lib/usage";
7675
import {
7776
AssigneeIdentityIdentifier,
7877
TeamSubscription,
@@ -107,13 +106,13 @@ import { BitbucketAppSupport } from "../bitbucket/bitbucket-app-support";
107106
import { URL } from "url";
108107
import { UserCounter } from "../user/user-counter";
109108
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
110-
import { CachingUsageServiceClientProvider } from "@gitpod/usage-api/lib/usage/v1/sugar";
111-
import * as usage from "@gitpod/usage-api/lib/usage/v1/usage_pb";
109+
import { CachingUsageServiceClientProvider, UsageService } from "@gitpod/usage-api/lib/usage/v1/sugar";
112110
import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb";
113111
import { EntitlementService } from "../../../src/billing/entitlement-service";
114112
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
115113
import { BillingModes } from "../billing/billing-mode";
116114
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
115+
import { BillingService } from "../billing/billing-service";
117116

118117
@injectable()
119118
export class GitpodServerEEImpl extends GitpodServerImpl {
@@ -160,7 +159,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
160159
@inject(EntitlementService) protected readonly entitlementService: EntitlementService;
161160

162161
@inject(BillingModes) protected readonly billingModes: BillingModes;
163-
162+
@inject(BillingService) protected readonly billingService: BillingService;
164163
initialize(
165164
client: GitpodClient | undefined,
166165
user: User | undefined,
@@ -256,39 +255,22 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
256255
): Promise<void> {
257256
await super.mayStartWorkspace(ctx, user, runningInstances);
258257

259-
// TODO(at) replace the naive implementation based on usage service
260-
// with a proper call check against the upcoming invoice.
261-
// For now this should just enable the work on fronend.
262-
if (await this.isUsageBasedFeatureFlagEnabled(user)) {
263-
// dummy implementation to test frontend bits
264-
const attributionId = await this.userService.getWorkspaceUsageAttributionId(user);
265-
const costCenter = !!attributionId && (await this.costCenterDB.findById(attributionId));
266-
if (costCenter) {
267-
const allSessions = await this.listBilledUsage(ctx, {
268-
attributionId,
269-
startedTimeOrder: SortOrder.Descending,
270-
});
271-
const totalUsage = allSessions.map((s) => s.credits).reduce((a, b) => a + b, 0);
272-
273-
if (totalUsage >= costCenter.spendingLimit) {
274-
throw new ResponseError(
275-
ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED,
276-
"Increase spending limit and try again.",
277-
{
278-
attributionId: user.usageAttributionId,
279-
},
280-
);
281-
}
282-
}
283-
}
284-
285258
const result = await this.entitlementService.mayStartWorkspace(user, new Date(), runningInstances);
286259
if (!result.enoughCredits) {
287260
throw new ResponseError(
288261
ErrorCodes.NOT_ENOUGH_CREDIT,
289262
`Not enough monthly workspace hours. Please upgrade your account to get more hours for your workspaces.`,
290263
);
291264
}
265+
if (result.spendingLimitReachedOnCostCenter) {
266+
throw new ResponseError(
267+
ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED,
268+
"Increase spending limit and try again.",
269+
{
270+
attributionId: result.spendingLimitReachedOnCostCenter,
271+
},
272+
);
273+
}
292274
if (!!result.hitParallelWorkspaceLimit) {
293275
throw new ResponseError(
294276
ErrorCodes.TOO_MANY_RUNNING_WORKSPACES,
@@ -2146,24 +2128,17 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
21462128
async getNotifications(ctx: TraceContext): Promise<string[]> {
21472129
const result = await super.getNotifications(ctx);
21482130
const user = this.checkAndBlockUser("getNotifications");
2149-
if (user.usageAttributionId) {
2150-
// This change doesn't matter much because the listBilledUsage() call
2151-
// will be removed anyway in https://github.com/gitpod-io/gitpod/issues/11692
2152-
const request = {
2153-
attributionId: user.usageAttributionId,
2154-
startedTimeOrder: SortOrder.Descending,
2155-
};
2156-
const allSessions = await this.listBilledUsage(ctx, request);
2157-
const totalUsage = allSessions.map((s) => s.credits).reduce((a, b) => a + b, 0);
2158-
const costCenter = await this.costCenterDB.findById(user.usageAttributionId);
2131+
2132+
const billingMode = await this.billingModes.getBillingModeForUser(user, new Date());
2133+
if (billingMode.mode === "usage-based") {
2134+
const limit = await this.billingService.checkSpendingLimitReached(user);
2135+
const costCenter = await this.costCenterDB.findById(AttributionId.render(limit.attributionId));
21592136
if (costCenter) {
2160-
if (totalUsage > costCenter.spendingLimit) {
2137+
if (limit.reached) {
21612138
result.unshift("The spending limit is reached.");
2162-
} else if (totalUsage > costCenter.spendingLimit * 0.8) {
2139+
} else if (limit.almostReached) {
21632140
result.unshift("The spending limit is almost reached.");
21642141
}
2165-
} else {
2166-
log.warn("No costcenter found.", { userId: user.id, attributionId: user.usageAttributionId });
21672142
}
21682143
}
21692144
return result;
@@ -2192,7 +2167,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
21922167
timestampFrom,
21932168
timestampTo,
21942169
);
2195-
const sessions = response.getSessionsList().map((s) => this.mapBilledSession(s));
2170+
const sessions = response.getSessionsList().map((s) => UsageService.mapBilledSession(s));
21962171

21972172
return sessions;
21982173
}
@@ -2233,28 +2208,6 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
22332208
await this.guardAccess({ kind: "costCenter", /*subject: costCenter,*/ owner }, operation);
22342209
}
22352210

2236-
protected mapBilledSession(s: usage.BilledSession): BillableSession {
2237-
function mandatory<T>(v: T, m: (v: T) => string = (s) => "" + s): string {
2238-
if (!v) {
2239-
throw new Error(`Empty value in usage.BilledSession for instanceId '${s.getInstanceId()}'`);
2240-
}
2241-
return m(v);
2242-
}
2243-
return {
2244-
attributionId: mandatory(s.getAttributionId()),
2245-
userId: s.getUserId() || undefined,
2246-
teamId: s.getTeamId() || undefined,
2247-
projectId: s.getProjectId() || undefined,
2248-
workspaceId: mandatory(s.getWorkspaceId()),
2249-
instanceId: mandatory(s.getInstanceId()),
2250-
workspaceType: mandatory(s.getWorkspaceType()) as WorkspaceType,
2251-
workspaceClass: s.getWorkspaceClass(),
2252-
startTime: mandatory(s.getStartTime(), (t) => t!.toDate().toISOString()),
2253-
endTime: s.getEndTime()?.toDate().toISOString(),
2254-
credits: s.getCredits(), // optional
2255-
};
2256-
}
2257-
22582211
async getBillingModeForUser(ctx: TraceContextWithSpan): Promise<BillingMode> {
22592212
traceAPIParams(ctx, {});
22602213

0 commit comments

Comments
 (0)