Skip to content

Commit bdbe617

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

File tree

3 files changed

+67
-74
lines changed

3 files changed

+67
-74
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 & 67 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,6 +159,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
160159
@inject(EntitlementService) protected readonly entitlementService: EntitlementService;
161160

162161
@inject(BillingModes) protected readonly billingModes: BillingModes;
162+
@inject(BillingService) protected readonly billingService: BillingService;
163163

164164
initialize(
165165
client: GitpodClient | undefined,
@@ -256,39 +256,22 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
256256
): Promise<void> {
257257
await super.mayStartWorkspace(ctx, user, runningInstances);
258258

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-
285259
const result = await this.entitlementService.mayStartWorkspace(user, new Date(), runningInstances);
286260
if (!result.enoughCredits) {
287261
throw new ResponseError(
288262
ErrorCodes.NOT_ENOUGH_CREDIT,
289263
`Not enough monthly workspace hours. Please upgrade your account to get more hours for your workspaces.`,
290264
);
291265
}
266+
if (result.spendingLimitReachedOnCostCenter) {
267+
throw new ResponseError(
268+
ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED,
269+
"Increase spending limit and try again.",
270+
{
271+
attributionId: result.spendingLimitReachedOnCostCenter,
272+
},
273+
);
274+
}
292275
if (!!result.hitParallelWorkspaceLimit) {
293276
throw new ResponseError(
294277
ErrorCodes.TOO_MANY_RUNNING_WORKSPACES,
@@ -2146,24 +2129,17 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
21462129
async getNotifications(ctx: TraceContext): Promise<string[]> {
21472130
const result = await super.getNotifications(ctx);
21482131
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);
2132+
2133+
const billingMode = await this.billingModes.getBillingModeForUser(user, new Date());
2134+
if (billingMode.mode === "usage-based") {
2135+
const limit = await this.billingService.checkSpendingLimitReached(user);
2136+
const costCenter = await this.costCenterDB.findById(AttributionId.render(limit.attributionId));
21592137
if (costCenter) {
2160-
if (totalUsage > costCenter.spendingLimit) {
2138+
if (limit.reached) {
21612139
result.unshift("The spending limit is reached.");
2162-
} else if (totalUsage > costCenter.spendingLimit * 0.8) {
2140+
} else if (limit.almostReached) {
21632141
result.unshift("The spending limit is almost reached.");
21642142
}
2165-
} else {
2166-
log.warn("No costcenter found.", { userId: user.id, attributionId: user.usageAttributionId });
21672143
}
21682144
}
21692145
return result;
@@ -2192,7 +2168,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
21922168
timestampFrom,
21932169
timestampTo,
21942170
);
2195-
const sessions = response.getSessionsList().map((s) => this.mapBilledSession(s));
2171+
const sessions = response.getSessionsList().map((s) => UsageService.mapBilledSession(s));
21962172

21972173
return sessions;
21982174
}
@@ -2233,28 +2209,6 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
22332209
await this.guardAccess({ kind: "costCenter", /*subject: costCenter,*/ owner }, operation);
22342210
}
22352211

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-
22582212
async getBillingModeForUser(ctx: TraceContextWithSpan): Promise<BillingMode> {
22592213
traceAPIParams(ctx, {});
22602214

components/usage-api/typescript/src/usage/v1/sugar.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import { UsageServiceClient } from "./usage_grpc_pb";
88
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
99
import * as opentracing from "opentracing";
1010
import { Metadata } from "@grpc/grpc-js";
11-
import { ListBilledUsageRequest, ListBilledUsageResponse } from "./usage_pb";
11+
import { BilledSession, ListBilledUsageRequest, ListBilledUsageResponse } from "./usage_pb";
1212
import { injectable, inject, optional } from "inversify";
1313
import { createClientCallMetricsInterceptor, IClientCallMetrics } from "@gitpod/gitpod-protocol/lib/util/grpc";
1414
import * as grpc from "@grpc/grpc-js";
1515
import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb";
16+
import { BillableSession } from "@gitpod/gitpod-protocol/lib/usage";
17+
import { WorkspaceType } from "@gitpod/gitpod-protocol";
1618

1719
export const UsageServiceClientProvider = Symbol("UsageServiceClientProvider");
1820

@@ -91,7 +93,13 @@ export class PromisifiedUsageServiceClient {
9193
);
9294
}
9395

94-
public async listBilledUsage(_ctx: TraceContext, attributionId: string, order: ListBilledUsageRequest.Ordering, from?: Timestamp, to?: Timestamp): Promise<ListBilledUsageResponse> {
96+
public async listBilledUsage(
97+
_ctx: TraceContext,
98+
attributionId: string,
99+
order: ListBilledUsageRequest.Ordering,
100+
from?: Timestamp,
101+
to?: Timestamp,
102+
): Promise<ListBilledUsageResponse> {
95103
const ctx = TraceContext.childContext(`/usage-service/listBilledUsage`, _ctx);
96104

97105
try {
@@ -133,3 +141,27 @@ export class PromisifiedUsageServiceClient {
133141
};
134142
}
135143
}
144+
145+
export namespace UsageService {
146+
export function mapBilledSession(s: BilledSession): BillableSession {
147+
function mandatory<T>(v: T, m: (v: T) => string = (s) => "" + s): string {
148+
if (!v) {
149+
throw new Error(`Empty value in usage.BilledSession for instanceId '${s.getInstanceId()}'`);
150+
}
151+
return m(v);
152+
}
153+
return {
154+
attributionId: mandatory(s.getAttributionId()),
155+
userId: s.getUserId() || undefined,
156+
teamId: s.getTeamId() || undefined,
157+
projectId: s.getProjectId() || undefined,
158+
workspaceId: mandatory(s.getWorkspaceId()),
159+
instanceId: mandatory(s.getInstanceId()),
160+
workspaceType: mandatory(s.getWorkspaceType()) as WorkspaceType,
161+
workspaceClass: s.getWorkspaceClass(),
162+
startTime: mandatory(s.getStartTime(), (t) => t!.toDate().toISOString()),
163+
endTime: s.getEndTime()?.toDate().toISOString(),
164+
credits: s.getCredits(), // optional
165+
};
166+
}
167+
}

0 commit comments

Comments
 (0)