@@ -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" ;
@@ -211,6 +212,57 @@ export class UserService {
211
212
return false ;
212
213
}
213
214
215
+ protected async findTeamUsageBasedSubscriptionId ( team : Team ) : Promise < string | undefined > {
216
+ const customer = await this . stripeService . findCustomerByTeamId ( team . id ) ;
217
+ if ( ! customer ) {
218
+ return ;
219
+ }
220
+ const subscription = await this . stripeService . findUncancelledSubscriptionByCustomer ( customer . id ) ;
221
+ return subscription ?. id ;
222
+ }
223
+
224
+ protected async validateUsageAttributionId ( user : User , usageAttributionId : string ) : Promise < void > {
225
+ const attribution = AttributionId . parse ( usageAttributionId ) ;
226
+ if ( attribution ?. kind === "team" ) {
227
+ const team = await this . teamDB . findTeamById ( attribution . teamId ) ;
228
+ if ( ! team ) {
229
+ throw new Error ( "Selected team does not exist!" ) ;
230
+ }
231
+ const members = await this . teamDB . findMembersByTeam ( team . id ) ;
232
+ if ( ! members . find ( ( m ) => m . userId === user . id ) ) {
233
+ throw new Error ( "User is not part of selected team!" ) ;
234
+ }
235
+ const subscriptionId = await this . findTeamUsageBasedSubscriptionId ( team ) ;
236
+ if ( ! subscriptionId ) {
237
+ throw new Error ( "Selected team has no subscription!" ) ;
238
+ }
239
+ }
240
+ }
241
+
242
+ protected async findSingleTeamWithUsageBasedBilling ( user : User ) : Promise < Team | undefined > {
243
+ // Find all the user's teams with usage-based billing enabled.
244
+ const teams = await this . teamDB . findTeamsByUser ( user . id ) ;
245
+ const teamsWithBilling : Team [ ] = [ ] ;
246
+ await Promise . all (
247
+ teams . map ( async ( team ) => {
248
+ const subscriptionId = await this . findTeamUsageBasedSubscriptionId ( team ) ;
249
+ if ( subscriptionId ) {
250
+ teamsWithBilling . push ( team ) ;
251
+ }
252
+ } ) ,
253
+ ) ;
254
+ if ( teamsWithBilling . length > 1 ) {
255
+ // Multiple teams with usage-based billing enabled -- ask the user to make an explicit choice.
256
+ throw new Error ( "Multiple billing teams! Please choose one" ) ;
257
+ }
258
+ if ( teamsWithBilling . length === 1 ) {
259
+ // Single team with usage-based billing enabled -- attribute all usage to it.
260
+ return teamsWithBilling [ 0 ] ;
261
+ }
262
+ // No team with usage-based billing enabled.
263
+ return undefined ;
264
+ }
265
+
214
266
/**
215
267
* Identifies the team or user to which a workspace instance's running time should be attributed to
216
268
* (e.g. for usage analytics or billing purposes).
@@ -229,12 +281,18 @@ export class UserService {
229
281
async getWorkspaceUsageAttributionId ( user : User , projectId ?: string ) : Promise < string | undefined > {
230
282
// A. Billing-based attribution
231
283
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 } ) ;
284
+ if ( user . usageAttributionId ) {
285
+ await this . validateUsageAttributionId ( user , user . usageAttributionId ) ;
286
+ // Return the user's explicit attribution ID.
287
+ return user . usageAttributionId ;
235
288
}
236
- // Return the user's explicit attribution ID.
237
- return user . usageAttributionId ;
289
+ const billingTeam = await this . findSingleTeamWithUsageBasedBilling ( user ) ;
290
+ if ( billingTeam ) {
291
+ // Single team with usage-based billing enabled -- attribute all usage to it.
292
+ return AttributionId . render ( { kind : "team" , teamId : billingTeam . id } ) ;
293
+ }
294
+ // Attribute all usage to the user by default (regardless of project/team).
295
+ return AttributionId . render ( { kind : "user" , userId : user . id } ) ;
238
296
}
239
297
240
298
// B. Project-based attribution
0 commit comments