@@ -15,6 +15,7 @@ import {
15
15
WORKSPACE_TIMEOUT_DEFAULT_LONG ,
16
16
WORKSPACE_TIMEOUT_EXTENDED ,
17
17
WORKSPACE_TIMEOUT_EXTENDED_ALT ,
18
+ Team ,
18
19
} from "@gitpod/gitpod-protocol" ;
19
20
import { CostCenterDB , ProjectDB , TeamDB , TermsAcceptanceDB , UserDB } from "@gitpod/gitpod-db/lib" ;
20
21
import { HostContextProvider } from "../auth/host-context-provider" ;
@@ -29,6 +30,8 @@ import { EmailAddressAlreadyTakenException, SelectAccountException } from "../au
29
30
import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth" ;
30
31
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution" ;
31
32
import { StripeService } from "../../ee/src/user/stripe-service" ;
33
+ import { ResponseError } from "vscode-ws-jsonrpc" ;
34
+ import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error" ;
32
35
33
36
export interface FindUserByIdentityStrResult {
34
37
user : User ;
@@ -211,6 +214,66 @@ export class UserService {
211
214
return false ;
212
215
}
213
216
217
+ protected async findTeamUsageBasedSubscriptionId ( team : Team ) : Promise < string | undefined > {
218
+ const customer = await this . stripeService . findCustomerByTeamId ( team . id ) ;
219
+ if ( ! customer ) {
220
+ return ;
221
+ }
222
+ const subscription = await this . stripeService . findUncancelledSubscriptionByCustomer ( customer . id ) ;
223
+ return subscription ?. id ;
224
+ }
225
+
226
+ protected async validateUsageAttributionId ( user : User , usageAttributionId : string ) : Promise < void > {
227
+ const attribution = AttributionId . parse ( usageAttributionId ) ;
228
+ if ( attribution ?. kind === "team" ) {
229
+ const team = await this . teamDB . findTeamById ( attribution . teamId ) ;
230
+ if ( ! team ) {
231
+ throw new ResponseError (
232
+ ErrorCodes . INVALID_COST_CENTER ,
233
+ "The billing team you've selected no longer exists." ,
234
+ ) ;
235
+ }
236
+ const members = await this . teamDB . findMembersByTeam ( team . id ) ;
237
+ if ( ! members . find ( ( m ) => m . userId === user . id ) ) {
238
+ throw new ResponseError (
239
+ ErrorCodes . INVALID_COST_CENTER ,
240
+ "You're no longer a member of the selected billing team." ,
241
+ ) ;
242
+ }
243
+ const subscriptionId = await this . findTeamUsageBasedSubscriptionId ( team ) ;
244
+ if ( ! subscriptionId ) {
245
+ throw new ResponseError (
246
+ ErrorCodes . INVALID_COST_CENTER ,
247
+ "The billing team you've selected has no active subscription." ,
248
+ ) ;
249
+ }
250
+ }
251
+ }
252
+
253
+ protected async findSingleTeamWithUsageBasedBilling ( user : User ) : Promise < Team | undefined > {
254
+ // Find all the user's teams with usage-based billing enabled.
255
+ const teams = await this . teamDB . findTeamsByUser ( user . id ) ;
256
+ const teamsWithBilling : Team [ ] = [ ] ;
257
+ await Promise . all (
258
+ teams . map ( async ( team ) => {
259
+ const subscriptionId = await this . findTeamUsageBasedSubscriptionId ( team ) ;
260
+ if ( subscriptionId ) {
261
+ teamsWithBilling . push ( team ) ;
262
+ }
263
+ } ) ,
264
+ ) ;
265
+ if ( teamsWithBilling . length > 1 ) {
266
+ // Multiple teams with usage-based billing enabled -- ask the user to make an explicit choice.
267
+ throw new ResponseError ( ErrorCodes . INVALID_COST_CENTER , "Multiple teams have billing enabled." ) ;
268
+ }
269
+ if ( teamsWithBilling . length === 1 ) {
270
+ // Single team with usage-based billing enabled -- attribute all usage to it.
271
+ return teamsWithBilling [ 0 ] ;
272
+ }
273
+ // No team with usage-based billing enabled.
274
+ return undefined ;
275
+ }
276
+
214
277
/**
215
278
* Identifies the team or user to which a workspace instance's running time should be attributed to
216
279
* (e.g. for usage analytics or billing purposes).
@@ -229,12 +292,18 @@ export class UserService {
229
292
async getWorkspaceUsageAttributionId ( user : User , projectId ?: string ) : Promise < string | undefined > {
230
293
// A. Billing-based attribution
231
294
if ( this . config . enablePayment ) {
232
- if ( ! user . usageAttributionId ) {
233
- // No explicit user attribution ID yet -- attribute all usage to the user by default (regardless of project/team).
234
- return AttributionId . render ( { kind : "user" , userId : user . id } ) ;
295
+ if ( user . usageAttributionId ) {
296
+ await this . validateUsageAttributionId ( user , user . usageAttributionId ) ;
297
+ // Return the user's explicit attribution ID.
298
+ return user . usageAttributionId ;
235
299
}
236
- // Return the user's explicit attribution ID.
237
- return user . usageAttributionId ;
300
+ const billingTeam = await this . findSingleTeamWithUsageBasedBilling ( user ) ;
301
+ if ( billingTeam ) {
302
+ // Single team with usage-based billing enabled -- attribute all usage to it.
303
+ return AttributionId . render ( { kind : "team" , teamId : billingTeam . id } ) ;
304
+ }
305
+ // Attribute all usage to the user by default (regardless of project/team).
306
+ return AttributionId . render ( { kind : "user" , userId : user . id } ) ;
238
307
}
239
308
240
309
// B. Project-based attribution
0 commit comments