From 7513c35941261404b0b1b69dee107408dd3941a6 Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Tue, 28 Jun 2022 12:31:05 +0000 Subject: [PATCH 1/3] [server] When a user with no explicit 'usageAttributionId' joins a team with usage-based billing enabled, automatically re-attribute usage to that team --- .../ee/src/workspace/gitpod-server-impl.ts | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 8c2f4db77e9d58..6dcc4c002fa654 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -1428,13 +1428,27 @@ export class GitpodServerEEImpl extends GitpodServerImpl { protected async onTeamMemberAdded(userId: string, teamId: string): Promise { const now = new Date(); - const teamSubscription = await this.teamSubscription2DB.findForTeam(teamId, now.toISOString()); - if (!teamSubscription) { - // No team subscription, nothing to do 🌴 - return; + const ts2 = await this.teamSubscription2DB.findForTeam(teamId, now.toISOString()); + if (ts2) { + await this.updateTeamSubscriptionQuantity(ts2); + await this.teamSubscription2Service.addTeamMemberSubscription(ts2, userId); + } + const [teamCustomer, user] = await Promise.all([ + this.stripeService.findCustomerByTeamId(teamId), + this.userDB.findUserById(userId), + ]); + if (teamCustomer && user && !user.additionalData?.usageAttributionId) { + // If the user didn't explicitly choose yet where their usage should be attributed to, and + // they join a team which accepts usage attribution (i.e. with usage-based billing enabled), + // then we simplify the UX by automatically attributing the user's usage to that team. + // Note: This default choice can be changed at any time by the user in their billing settings. + const subscription = await this.stripeService.findUncancelledSubscriptionByCustomer(teamCustomer.id); + if (subscription) { + user.additionalData = user.additionalData || {}; + user.additionalData.usageAttributionId = `team:${teamId}`; + await this.userDB.updateUserPartial(user); + } } - await this.updateTeamSubscriptionQuantity(teamSubscription); - await this.teamSubscription2Service.addTeamMemberSubscription(teamSubscription, userId); } protected async onTeamMemberRemoved(userId: string, teamId: string, teamMembershipId: string): Promise { From 20b077b5363bb69c6fe0d52772a279fa39e5db14 Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Tue, 28 Jun 2022 12:31:42 +0000 Subject: [PATCH 2/3] [server] When a user attributes all their usage to a team, but then leaves that team, reset their selected 'usageAttributionId' --- .../ee/src/workspace/gitpod-server-impl.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 6dcc4c002fa654..18d0dcd0c28182 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -1453,18 +1453,19 @@ export class GitpodServerEEImpl extends GitpodServerImpl { protected async onTeamMemberRemoved(userId: string, teamId: string, teamMembershipId: string): Promise { const now = new Date(); - const teamSubscription = await this.teamSubscription2DB.findForTeam(teamId, now.toISOString()); - if (!teamSubscription) { - // No team subscription, nothing to do 🌴 - return; + const ts2 = await this.teamSubscription2DB.findForTeam(teamId, now.toISOString()); + if (ts2) { + await this.updateTeamSubscriptionQuantity(ts2); + await this.teamSubscription2Service.cancelTeamMemberSubscription(ts2, userId, teamMembershipId, now); + } + const user = await this.userDB.findUserById(userId); + if (user && user.additionalData?.usageAttributionId === `team:${teamId}`) { + // If the user previously attributed all their usage to a given team, but they are now leaving this + // team, then the currently selected usage attribution ID is no longer valid. In this case, we must + // reset this ID to the default value. + user.additionalData.usageAttributionId = undefined; + await this.userDB.updateUserPartial(user); } - await this.updateTeamSubscriptionQuantity(teamSubscription); - await this.teamSubscription2Service.cancelTeamMemberSubscription( - teamSubscription, - userId, - teamMembershipId, - now, - ); } protected async onTeamDeleted(teamId: string): Promise { From f1aa0f9d168bd97ee41974419bee4be3fca54280 Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Wed, 29 Jun 2022 08:15:26 +0000 Subject: [PATCH 3/3] [server] When a team enables usage-based billing, automatically re-attribute member usage to that team when the member has no explicit 'usageAttributionId' yet --- .../server/ee/src/workspace/gitpod-server-impl.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 18d0dcd0c28182..7febe4ba935ee3 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -1960,6 +1960,20 @@ export class GitpodServerEEImpl extends GitpodServerImpl { customer = await this.stripeService.createCustomerForTeam(user, team!, setupIntentId); } await this.stripeService.createSubscriptionForCustomer(customer.id, currency); + // For all team members that didn't explicitly choose yet where their usage should be attributed to, + // we simplify the UX by automatically attributing their usage to this recently-upgraded team. + // Note: This default choice can be changed at any time by members in their personal billing settings. + const members = await this.teamDB.findMembersByTeam(teamId); + await Promise.all( + members.map(async (m) => { + const u = await this.userDB.findUserById(m.userId); + if (u && !u.additionalData?.usageAttributionId) { + u.additionalData = u.additionalData || {}; + u.additionalData.usageAttributionId = `team:${teamId}`; + await this.userDB.updateUserPartial(u); + } + }), + ); } catch (error) { log.error(`Failed to subscribe team '${teamId}' to Stripe`, error); throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, `Failed to subscribe team '${teamId}' to Stripe`);