From 3a0dcb454f0a71b06b573004d2f429508fdd57e6 Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Thu, 15 Sep 2022 13:17:47 +0000 Subject: [PATCH 1/7] [server] Set Stripe customer country when attaching a payment method --- components/server/ee/src/user/stripe-service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/server/ee/src/user/stripe-service.ts b/components/server/ee/src/user/stripe-service.ts index 550b9a6c7fc2c1..36688b8ad84147 100644 --- a/components/server/ee/src/user/stripe-service.ts +++ b/components/server/ee/src/user/stripe-service.ts @@ -115,8 +115,12 @@ export class StripeService { await this.getStripe().paymentMethods.attach(setupIntent.payment_method, { customer: customer.id, }); + const paymentMethod = await this.getStripe().paymentMethods.retrieve(setupIntent.payment_method); await this.getStripe().customers.update(customer.id, { invoice_settings: { default_payment_method: setupIntent.payment_method }, + ...(paymentMethod.billing_details.address?.country + ? { address: { line1: "", country: paymentMethod.billing_details.address?.country } } + : {}), }); } From ea8523fccaafebe743ca45c87a84a96df072089f Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Mon, 19 Sep 2022 09:05:08 +0000 Subject: [PATCH 2/7] [server] Refactor StripeService.findCustomerByQuery to .findCustomerByAttributionId --- .../server/ee/src/user/stripe-service.ts | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/components/server/ee/src/user/stripe-service.ts b/components/server/ee/src/user/stripe-service.ts index 36688b8ad84147..dd120f8a0070be 100644 --- a/components/server/ee/src/user/stripe-service.ts +++ b/components/server/ee/src/user/stripe-service.ts @@ -13,8 +13,6 @@ import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; const POLL_CREATED_CUSTOMER_INTERVAL_MS = 1000; const POLL_CREATED_CUSTOMER_MAX_ATTEMPTS = 30; -const ATTRIBUTION_ID_METADATA_KEY = "attributionId"; - @injectable() export class StripeService { @inject(Config) protected readonly config: Config; @@ -36,18 +34,15 @@ export class StripeService { } async findCustomerByUserId(userId: string): Promise { - return this.findCustomerByQuery( - `metadata['${ATTRIBUTION_ID_METADATA_KEY}']:'${AttributionId.render({ kind: "user", userId })}'`, - ); + return this.findCustomerByAttributionId(AttributionId.render({ kind: "user", userId })); } async findCustomerByTeamId(teamId: string): Promise { - return this.findCustomerByQuery( - `metadata['${ATTRIBUTION_ID_METADATA_KEY}']:'${AttributionId.render({ kind: "team", teamId })}'`, - ); + return this.findCustomerByAttributionId(AttributionId.render({ kind: "team", teamId })); } - async findCustomerByQuery(query: string): Promise { + async findCustomerByAttributionId(attributionId: string): Promise { + const query = `metadata['attributionId']:'${attributionId}'`; const result = await this.getStripe().customers.search({ query }); if (result.data.length > 1) { throw new Error(`Found more than one Stripe customer for query '${query}'`); @@ -56,20 +51,19 @@ export class StripeService { } async createCustomerForUser(user: User): Promise { - if (await this.findCustomerByUserId(user.id)) { + const attributionId = AttributionId.render({ kind: "user", userId: user.id }); + if (await this.findCustomerByAttributionId(attributionId)) { throw new Error(`A Stripe customer already exists for user '${user.id}'`); } // Create the customer in Stripe const customer = await this.getStripe().customers.create({ email: User.getPrimaryEmail(user), name: User.getName(user), - metadata: { - [ATTRIBUTION_ID_METADATA_KEY]: AttributionId.render({ kind: "user", userId: user.id }), - }, + metadata: { attributionId }, }); // Wait for the customer to show up in Stripe search results before proceeding let attempts = 0; - while (!(await this.findCustomerByUserId(user.id))) { + while (!(await this.findCustomerByAttributionId(attributionId))) { await new Promise((resolve) => setTimeout(resolve, POLL_CREATED_CUSTOMER_INTERVAL_MS)); if (++attempts > POLL_CREATED_CUSTOMER_MAX_ATTEMPTS) { throw new Error(`Could not confirm Stripe customer creation for user '${user.id}'`); @@ -79,7 +73,8 @@ export class StripeService { } async createCustomerForTeam(user: User, team: Team): Promise { - if (await this.findCustomerByTeamId(team.id)) { + const attributionId = AttributionId.render({ kind: "team", teamId: team.id }); + if (await this.findCustomerByAttributionId(attributionId)) { throw new Error(`A Stripe customer already exists for team '${team.id}'`); } // Create the customer in Stripe @@ -87,13 +82,11 @@ export class StripeService { const customer = await this.getStripe().customers.create({ email: User.getPrimaryEmail(user), name: userName ? `${userName} (${team.name})` : team.name, - metadata: { - [ATTRIBUTION_ID_METADATA_KEY]: AttributionId.render({ kind: "team", teamId: team.id }), - }, + metadata: { attributionId }, }); // Wait for the customer to show up in Stripe search results before proceeding let attempts = 0; - while (!(await this.findCustomerByTeamId(team.id))) { + while (!(await this.findCustomerByAttributionId(attributionId))) { await new Promise((resolve) => setTimeout(resolve, POLL_CREATED_CUSTOMER_INTERVAL_MS)); if (++attempts > POLL_CREATED_CUSTOMER_MAX_ATTEMPTS) { throw new Error(`Could not confirm Stripe customer creation for team '${team.id}'`); From 90999883c7eada4b8f86139f28839ad2a900e664 Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Thu, 15 Sep 2022 15:38:23 +0000 Subject: [PATCH 3/7] [server] Refactor StripeService to pass around IDs instead of (possibly outdated) objects --- .../ee/src/billing/billing-mode.spec.db.ts | 12 ++--- .../server/ee/src/billing/billing-mode.ts | 16 +++--- .../ee/src/billing/entitlement-service-ubp.ts | 12 ++--- .../server/ee/src/user/stripe-service.ts | 48 +++++++++--------- .../ee/src/workspace/gitpod-server-impl.ts | 49 ++++++++----------- components/server/src/user/user-service.ts | 8 +-- 6 files changed, 70 insertions(+), 75 deletions(-) diff --git a/components/server/ee/src/billing/billing-mode.spec.db.ts b/components/server/ee/src/billing/billing-mode.spec.db.ts index e21378c96657cc..b4666a8a2d6394 100644 --- a/components/server/ee/src/billing/billing-mode.spec.db.ts +++ b/components/server/ee/src/billing/billing-mode.spec.db.ts @@ -50,24 +50,22 @@ class StripeServiceMock extends StripeService { super(); } - async findUncancelledSubscriptionByCustomer(customerId: string): Promise { + async findUncancelledSubscriptionByCustomer(customerId: string): Promise { if (this.subscription?.customer === customerId) { - return this.subscription as Stripe.Subscription; + return this.subscription.id; } return undefined; } - async findCustomerByUserId(userId: string): Promise { + async findCustomerByUserId(userId: string): Promise { const customerId = this.subscription?.customer; if (!customerId) { return undefined; } - return { - id: customerId, - } as Stripe.Customer; + return customerId; } - async findCustomerByTeamId(teamId: string): Promise { + async findCustomerByTeamId(teamId: string): Promise { return this.findCustomerByUserId(teamId); } } diff --git a/components/server/ee/src/billing/billing-mode.ts b/components/server/ee/src/billing/billing-mode.ts index dbbb5ac0a66385..77e2d4b6d47f18 100644 --- a/components/server/ee/src/billing/billing-mode.ts +++ b/components/server/ee/src/billing/billing-mode.ts @@ -116,10 +116,10 @@ export class BillingModesImpl implements BillingModes { // Stripe: Active personal subsciption? let hasUbbPersonal = false; - const customer = await this.stripeSvc.findCustomerByUserId(user.id); - if (customer) { - const subscription = await this.stripeSvc.findUncancelledSubscriptionByCustomer(customer.id); - if (subscription) { + const customerId = await this.stripeSvc.findCustomerByUserId(user.id); + if (customerId) { + const subscriptionId = await this.stripeSvc.findUncancelledSubscriptionByCustomer(customerId); + if (subscriptionId) { hasUbbPersonal = true; } } @@ -192,10 +192,10 @@ export class BillingModesImpl implements BillingModes { // 3. Now we're usage-based. We only have to figure out whether we have a plan yet or not. const result: BillingMode = { mode: "usage-based" }; - const customer = await this.stripeSvc.findCustomerByTeamId(team.id); - if (customer) { - const subscription = await this.stripeSvc.findUncancelledSubscriptionByCustomer(customer.id); - if (subscription) { + const customerId = await this.stripeSvc.findCustomerByTeamId(team.id); + if (customerId) { + const subscriptionId = await this.stripeSvc.findUncancelledSubscriptionByCustomer(customerId); + if (subscriptionId) { result.paid = true; } } diff --git a/components/server/ee/src/billing/entitlement-service-ubp.ts b/components/server/ee/src/billing/entitlement-service-ubp.ts index af2957474a4e43..eca01be23f1b50 100644 --- a/components/server/ee/src/billing/entitlement-service-ubp.ts +++ b/components/server/ee/src/billing/entitlement-service-ubp.ts @@ -114,9 +114,9 @@ export class EntitlementServiceUBP implements EntitlementService { protected async hasPaidSubscription(user: User, date: Date): Promise { // Paid user? - const customer = await this.stripeService.findCustomerByUserId(user.id); - if (customer) { - const subscriptionId = await this.stripeService.findUncancelledSubscriptionByCustomer(customer.id); + const customerId = await this.stripeService.findCustomerByUserId(user.id); + if (customerId) { + const subscriptionId = await this.stripeService.findUncancelledSubscriptionByCustomer(customerId); if (subscriptionId) { return true; } @@ -124,11 +124,11 @@ export class EntitlementServiceUBP implements EntitlementService { // Member of paid team? const teams = await this.teamDB.findTeamsByUser(user.id); const isTeamSubscribedPromises = teams.map(async (team: Team) => { - const customer = await this.stripeService.findCustomerByTeamId(team.id); - if (!customer) { + const customerId = await this.stripeService.findCustomerByTeamId(team.id); + if (!customerId) { return false; } - const subscriptionId = await this.stripeService.findUncancelledSubscriptionByCustomer(customer.id); + const subscriptionId = await this.stripeService.findUncancelledSubscriptionByCustomer(customerId); return !!subscriptionId; }); // Return the first truthy promise, or false if all the promises were falsy. diff --git a/components/server/ee/src/user/stripe-service.ts b/components/server/ee/src/user/stripe-service.ts index dd120f8a0070be..81e8c74af71ec7 100644 --- a/components/server/ee/src/user/stripe-service.ts +++ b/components/server/ee/src/user/stripe-service.ts @@ -33,24 +33,24 @@ export class StripeService { return await this.getStripe().setupIntents.create({ usage: "on_session" }); } - async findCustomerByUserId(userId: string): Promise { + async findCustomerByUserId(userId: string): Promise { return this.findCustomerByAttributionId(AttributionId.render({ kind: "user", userId })); } - async findCustomerByTeamId(teamId: string): Promise { + async findCustomerByTeamId(teamId: string): Promise { return this.findCustomerByAttributionId(AttributionId.render({ kind: "team", teamId })); } - async findCustomerByAttributionId(attributionId: string): Promise { + async findCustomerByAttributionId(attributionId: string): Promise { const query = `metadata['attributionId']:'${attributionId}'`; const result = await this.getStripe().customers.search({ query }); if (result.data.length > 1) { throw new Error(`Found more than one Stripe customer for query '${query}'`); } - return result.data[0]; + return result.data[0]?.id; } - async createCustomerForUser(user: User): Promise { + async createCustomerForUser(user: User): Promise { const attributionId = AttributionId.render({ kind: "user", userId: user.id }); if (await this.findCustomerByAttributionId(attributionId)) { throw new Error(`A Stripe customer already exists for user '${user.id}'`); @@ -69,10 +69,10 @@ export class StripeService { throw new Error(`Could not confirm Stripe customer creation for user '${user.id}'`); } } - return customer; + return customer.id; } - async createCustomerForTeam(user: User, team: Team): Promise { + async createCustomerForTeam(user: User, team: Team): Promise { const attributionId = AttributionId.render({ kind: "team", teamId: team.id }); if (await this.findCustomerByAttributionId(attributionId)) { throw new Error(`A Stripe customer already exists for team '${team.id}'`); @@ -92,24 +92,24 @@ export class StripeService { throw new Error(`Could not confirm Stripe customer creation for team '${team.id}'`); } } - return customer; + return customer.id; } - async setPreferredCurrencyForCustomer(customer: Stripe.Customer, currency: string): Promise { - await this.getStripe().customers.update(customer.id, { metadata: { preferredCurrency: currency } }); + async setPreferredCurrencyForCustomer(customerId: string, currency: string): Promise { + await this.getStripe().customers.update(customerId, { metadata: { preferredCurrency: currency } }); } - async setDefaultPaymentMethodForCustomer(customer: Stripe.Customer, setupIntentId: string): Promise { + async setDefaultPaymentMethodForCustomer(customerId: string, setupIntentId: string): Promise { const setupIntent = await this.getStripe().setupIntents.retrieve(setupIntentId); if (typeof setupIntent.payment_method !== "string") { throw new Error("The provided Stripe SetupIntent does not have a valid payment method attached"); } // Attach the provided payment method to the customer await this.getStripe().paymentMethods.attach(setupIntent.payment_method, { - customer: customer.id, + customer: customerId, }); const paymentMethod = await this.getStripe().paymentMethods.retrieve(setupIntent.payment_method); - await this.getStripe().customers.update(customer.id, { + await this.getStripe().customers.update(customerId, { invoice_settings: { default_payment_method: setupIntent.payment_method }, ...(paymentMethod.billing_details.address?.country ? { address: { line1: "", country: paymentMethod.billing_details.address?.country } } @@ -118,44 +118,48 @@ export class StripeService { } async getPortalUrlForTeam(team: Team): Promise { - const customer = await this.findCustomerByTeamId(team.id); - if (!customer) { + const customerId = await this.findCustomerByTeamId(team.id); + if (!customerId) { throw new Error(`No Stripe Customer ID found for team '${team.id}'`); } const session = await this.getStripe().billingPortal.sessions.create({ - customer: customer.id, + customer: customerId, return_url: this.config.hostUrl.with(() => ({ pathname: `/t/${team.slug}/billing` })).toString(), }); return session.url; } async getPortalUrlForUser(user: User): Promise { - const customer = await this.findCustomerByUserId(user.id); - if (!customer) { + const customerId = await this.findCustomerByUserId(user.id); + if (!customerId) { throw new Error(`No Stripe Customer ID found for user '${user.id}'`); } const session = await this.getStripe().billingPortal.sessions.create({ - customer: customer.id, + customer: customerId, return_url: this.config.hostUrl.with(() => ({ pathname: `/billing` })).toString(), }); return session.url; } - async findUncancelledSubscriptionByCustomer(customerId: string): Promise { + async findUncancelledSubscriptionByCustomer(customerId: string): Promise { const result = await this.getStripe().subscriptions.list({ customer: customerId, }); if (result.data.length > 1) { throw new Error(`Stripe customer '${customerId}') has more than one subscription!`); } - return result.data[0]; + return result.data[0]?.id; } async cancelSubscription(subscriptionId: string): Promise { await this.getStripe().subscriptions.del(subscriptionId); } - async createSubscriptionForCustomer(customer: Stripe.Customer): Promise { + async createSubscriptionForCustomer(customerId: string): Promise { + const customer = await this.getStripe().customers.retrieve(customerId, { expand: ["tax"] }); + if (!customer || customer.deleted) { + throw new Error(`Stripe customer '${customerId}' was deleted`); + } const currency = customer.metadata.preferredCurrency || "USD"; const priceId = this.config?.stripeConfig?.usageProductPriceIds[currency]; if (!priceId) { diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index b3ddc394f609d0..6667c935a7e8f3 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -115,7 +115,6 @@ import { EntitlementService, MayStartWorkspaceResult } from "../../../src/billin import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode"; import { BillingModes } from "../billing/billing-mode"; import { BillingService } from "../billing/billing-service"; -import Stripe from "stripe"; import { UsageServiceDefinition } from "@gitpod/usage-api/lib/usage/v1/usage.pb"; import { MessageBusIntegration } from "../../../src/workspace/messagebus-integration"; @@ -1597,11 +1596,11 @@ export class GitpodServerEEImpl extends GitpodServerImpl { { teamId, chargebeeSubscriptionId }, ); } - const teamCustomer = await this.stripeService.findCustomerByTeamId(teamId); - if (teamCustomer) { - const subsciption = await this.stripeService.findUncancelledSubscriptionByCustomer(teamCustomer.id); - if (subsciption) { - await this.stripeService.cancelSubscription(subsciption.id); + const teamCustomerId = await this.stripeService.findCustomerByTeamId(teamId); + if (teamCustomerId) { + const subsciptionId = await this.stripeService.findUncancelledSubscriptionByCustomer(teamCustomerId); + if (subsciptionId) { + await this.stripeService.cancelSubscription(subsciptionId); } } } @@ -2063,21 +2062,17 @@ export class GitpodServerEEImpl extends GitpodServerImpl { this.checkAndBlockUser("findStripeSubscriptionId"); - let customer: Stripe.Customer | undefined; try { if (attrId.kind == "team") { await this.guardTeamOperation(attrId.teamId, "get"); - customer = await this.stripeService.findCustomerByTeamId(attrId.teamId); - } else { - customer = await this.stripeService.findCustomerByUserId(attrId.userId); } - - if (!customer?.id) { + const customerId = await this.stripeService.findCustomerByAttributionId(attributionId); + if (!customerId) { return undefined; } - const subscription = await this.stripeService.findUncancelledSubscriptionByCustomer(customer.id); - return subscription?.id; + const subscriptionId = await this.stripeService.findUncancelledSubscriptionByCustomer(customerId); + return subscriptionId; } catch (error) { log.error(`Failed to get Stripe Subscription ID for '${attributionId}'`, error); throw new ResponseError( @@ -2092,11 +2087,11 @@ export class GitpodServerEEImpl extends GitpodServerImpl { const team = await this.guardTeamOperation(teamId, "update"); await this.ensureStripeApiIsAllowed({ team }); try { - let customer = await this.stripeService.findCustomerByTeamId(team!.id); - if (!customer) { - customer = await this.stripeService.createCustomerForTeam(user, team!); + let customerId = await this.stripeService.findCustomerByTeamId(team!.id); + if (!customerId) { + customerId = await this.stripeService.createCustomerForTeam(user, team!); } - await this.stripeService.setPreferredCurrencyForCustomer(customer, currency); + await this.stripeService.setPreferredCurrencyForCustomer(customerId, currency); } catch (error) { log.error(`Failed to update Stripe customer profile for team '${teamId}'`, error); throw new ResponseError( @@ -2110,11 +2105,11 @@ export class GitpodServerEEImpl extends GitpodServerImpl { const user = this.checkAndBlockUser("createOrUpdateStripeCustomerForUser"); await this.ensureStripeApiIsAllowed({ user }); try { - let customer = await this.stripeService.findCustomerByUserId(user.id); - if (!customer) { - customer = await this.stripeService.createCustomerForUser(user); + let customerId = await this.stripeService.findCustomerByUserId(user.id); + if (!customerId) { + customerId = await this.stripeService.createCustomerForUser(user); } - await this.stripeService.setPreferredCurrencyForCustomer(customer, currency); + await this.stripeService.setPreferredCurrencyForCustomer(customerId, currency); } catch (error) { log.error(`Failed to update Stripe customer profile for user '${user.id}'`, error); throw new ResponseError( @@ -2134,22 +2129,20 @@ export class GitpodServerEEImpl extends GitpodServerImpl { const user = this.checkAndBlockUser("subscribeToStripe"); - let customer: Stripe.Customer | undefined; try { if (attrId.kind === "team") { const team = await this.guardTeamOperation(attrId.teamId, "update"); await this.ensureStripeApiIsAllowed({ team }); - customer = await this.stripeService.findCustomerByTeamId(team!.id); } else { await this.ensureStripeApiIsAllowed({ user }); - customer = await this.stripeService.findCustomerByUserId(user.id); } - if (!customer) { + const customerId = await this.stripeService.findCustomerByAttributionId(attributionId); + if (!customerId) { throw new Error(`No Stripe customer profile for '${attributionId}'`); } - await this.stripeService.setDefaultPaymentMethodForCustomer(customer, setupIntentId); - await this.stripeService.createSubscriptionForCustomer(customer); + await this.stripeService.setDefaultPaymentMethodForCustomer(customerId, setupIntentId); + await this.stripeService.createSubscriptionForCustomer(customerId); // Creating a cost center for this team await this.usageService.setCostCenter({ diff --git a/components/server/src/user/user-service.ts b/components/server/src/user/user-service.ts index 2028dadd1bc98f..165cd8dd2338de 100644 --- a/components/server/src/user/user-service.ts +++ b/components/server/src/user/user-service.ts @@ -196,12 +196,12 @@ export class UserService { } protected async findTeamUsageBasedSubscriptionId(team: Team): Promise { - const customer = await this.stripeService.findCustomerByTeamId(team.id); - if (!customer) { + const customerId = await this.stripeService.findCustomerByTeamId(team.id); + if (!customerId) { return; } - const subscription = await this.stripeService.findUncancelledSubscriptionByCustomer(customer.id); - return subscription?.id; + const subscriptionId = await this.stripeService.findUncancelledSubscriptionByCustomer(customerId); + return subscriptionId; } protected async validateUsageAttributionId(user: User, usageAttributionId: string): Promise { From 7217ad3fe6b15f1f6ed3a97fa9c97ea95a33cb56 Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Thu, 15 Sep 2022 13:20:16 +0000 Subject: [PATCH 4/7] [server] Enable automatic tax on new Stripe subscriptions for customers in supported regions --- components/server/ee/src/user/stripe-service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/components/server/ee/src/user/stripe-service.ts b/components/server/ee/src/user/stripe-service.ts index 81e8c74af71ec7..790f149eb3f7ba 100644 --- a/components/server/ee/src/user/stripe-service.ts +++ b/components/server/ee/src/user/stripe-service.ts @@ -9,6 +9,7 @@ import Stripe from "stripe"; import { Team, User } from "@gitpod/gitpod-protocol"; import { Config } from "../../../src/config"; import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; const POLL_CREATED_CUSTOMER_INTERVAL_MS = 1000; const POLL_CREATED_CUSTOMER_MAX_ATTEMPTS = 30; @@ -158,18 +159,26 @@ export class StripeService { async createSubscriptionForCustomer(customerId: string): Promise { const customer = await this.getStripe().customers.retrieve(customerId, { expand: ["tax"] }); if (!customer || customer.deleted) { - throw new Error(`Stripe customer '${customerId}' was deleted`); + throw new Error(`Stripe customer '${customerId}' could not be found`); } const currency = customer.metadata.preferredCurrency || "USD"; const priceId = this.config?.stripeConfig?.usageProductPriceIds[currency]; if (!priceId) { throw new Error(`No Stripe Price ID configured for currency '${currency}'`); } + const isAutomaticTaxSupported = customer.tax?.automatic_tax === "supported"; + if (!isAutomaticTaxSupported) { + log.warn("Automatic Stripe tax is not supported for this customer", { + customerId, + taxInformation: customer.tax, + }); + } const startOfNextMonth = new Date(new Date().toISOString().slice(0, 7) + "-01"); // First day of this month (YYYY-MM-01) startOfNextMonth.setMonth(startOfNextMonth.getMonth() + 1); // Add one month await this.getStripe().subscriptions.create({ customer: customer.id, items: [{ price: priceId }], + automatic_tax: { enabled: isAutomaticTaxSupported }, billing_cycle_anchor: Math.round(startOfNextMonth.getTime() / 1000), }); } From cfa3bed1519754270ec6b3a8f215b69ad60bfc39 Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Wed, 21 Sep 2022 12:29:56 +0000 Subject: [PATCH 5/7] [server][dashboard] Refactor createOrUpdateStripeCustomerFor{User,Team} to createStripeCustomer --- .../components/UsageBasedBillingConfig.tsx | 9 +-- .../gitpod-protocol/src/gitpod-service.ts | 3 +- .../ee/src/workspace/gitpod-server-impl.ts | 55 ++++++++++--------- components/server/src/auth/rate-limiter.ts | 3 +- .../src/workspace/gitpod-server-impl.ts | 5 +- 5 files changed, 34 insertions(+), 41 deletions(-) diff --git a/components/dashboard/src/components/UsageBasedBillingConfig.tsx b/components/dashboard/src/components/UsageBasedBillingConfig.tsx index 5273b4fbf303c1..d5a41053e80175 100644 --- a/components/dashboard/src/components/UsageBasedBillingConfig.tsx +++ b/components/dashboard/src/components/UsageBasedBillingConfig.tsx @@ -295,13 +295,8 @@ function CreditCardInputForm(props: { attributionId: string }) { setBillingError(undefined); setIsLoading(true); try { - if (attrId.kind === "team") { - // Create Stripe customer for team & currency (or update currency) - await getGitpodService().server.createOrUpdateStripeCustomerForTeam(attrId.teamId, currency); - } else { - // Create Stripe customer for user & currency (or update currency) - await getGitpodService().server.createOrUpdateStripeCustomerForUser(currency); - } + // Create Stripe customer with currency + await getGitpodService().server.createStripeCustomer(props.attributionId, currency); const result = await stripe.confirmSetup({ elements, confirmParams: { diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 27334ac2fd50c0..f0cff6489a7853 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -287,8 +287,7 @@ export interface GitpodServer extends JsonRpcServer, AdminServer, getStripePublishableKey(): Promise; getStripeSetupIntentClientSecret(): Promise; findStripeSubscriptionId(attributionId: string): Promise; - createOrUpdateStripeCustomerForTeam(teamId: string, currency: string): Promise; - createOrUpdateStripeCustomerForUser(currency: string): Promise; + createStripeCustomer(attributionId: string, currency: string): Promise; subscribeToStripe(attributionId: string, setupIntentId: string): Promise; getStripePortalUrl(attributionId: string): Promise; getUsageLimit(attributionId: string): Promise; diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 6667c935a7e8f3..10b7a764209421 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -2082,39 +2082,42 @@ export class GitpodServerEEImpl extends GitpodServerImpl { } } - async createOrUpdateStripeCustomerForTeam(ctx: TraceContext, teamId: string, currency: string): Promise { - const user = this.checkAndBlockUser("createOrUpdateStripeCustomerForTeam"); - const team = await this.guardTeamOperation(teamId, "update"); - await this.ensureStripeApiIsAllowed({ team }); - try { - let customerId = await this.stripeService.findCustomerByTeamId(team!.id); - if (!customerId) { - customerId = await this.stripeService.createCustomerForTeam(user, team!); + async createStripeCustomer(ctx: TraceContext, attributionId: string, currency: string): Promise { + const user = this.checkAndBlockUser("createStripeCustomer"); + const attrId = AttributionId.parse(attributionId); + if (!attrId) { + throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attributionId '${attributionId}'`); + } + let team: Team | undefined; + if (attrId.kind === "team") { + team = await this.guardTeamOperation(attrId.teamId, "update"); + await this.ensureStripeApiIsAllowed({ team }); + } else { + if (attrId.userId !== user.id) { + throw new ResponseError( + ErrorCodes.PERMISSION_DENIED, + "Cannot create Stripe customer profile for another user", + ); } - await this.stripeService.setPreferredCurrencyForCustomer(customerId, currency); - } catch (error) { - log.error(`Failed to update Stripe customer profile for team '${teamId}'`, error); - throw new ResponseError( - ErrorCodes.INTERNAL_SERVER_ERROR, - `Failed to update Stripe customer profile for team '${teamId}'`, - ); + await this.ensureStripeApiIsAllowed({ user }); } - } - - async createOrUpdateStripeCustomerForUser(ctx: TraceContext, currency: string): Promise { - const user = this.checkAndBlockUser("createOrUpdateStripeCustomerForUser"); - await this.ensureStripeApiIsAllowed({ user }); try { - let customerId = await this.stripeService.findCustomerByUserId(user.id); - if (!customerId) { - customerId = await this.stripeService.createCustomerForUser(user); + if (await this.stripeService.findCustomerByAttributionId(attributionId)) { + throw new ResponseError( + ErrorCodes.BAD_REQUEST, + "A Stripe customer profile already exists for this attributionId", + ); } + const customerId = + attrId.kind === "team" + ? await this.stripeService.createCustomerForTeam(user, team!) + : await this.stripeService.createCustomerForUser(user); await this.stripeService.setPreferredCurrencyForCustomer(customerId, currency); } catch (error) { - log.error(`Failed to update Stripe customer profile for user '${user.id}'`, error); + log.error(`Failed to create Stripe customer profile for '${attributionId}'`, error); throw new ResponseError( ErrorCodes.INTERNAL_SERVER_ERROR, - `Failed to update Stripe customer profile for user '${user.id}'`, + `Failed to create Stripe customer profile for '${attributionId}'`, ); } } @@ -2144,7 +2147,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { await this.stripeService.setDefaultPaymentMethodForCustomer(customerId, setupIntentId); await this.stripeService.createSubscriptionForCustomer(customerId); - // Creating a cost center for this team + // Creating a cost center for this customer await this.usageService.setCostCenter({ costCenter: { attributionId: attributionId, diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index 245ef48f627755..3846f90dbf0862 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -201,8 +201,7 @@ const defaultFunctions: FunctionsConfig = { getStripePublishableKey: { group: "default", points: 1 }, getStripeSetupIntentClientSecret: { group: "default", points: 1 }, findStripeSubscriptionId: { group: "default", points: 1 }, - createOrUpdateStripeCustomerForTeam: { group: "default", points: 1 }, - createOrUpdateStripeCustomerForUser: { group: "default", points: 1 }, + createStripeCustomer: { group: "default", points: 1 }, subscribeToStripe: { group: "default", points: 1 }, getStripePortalUrl: { group: "default", points: 1 }, listUsage: { group: "default", points: 1 }, diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index affb78333435a1..fc4e9c84b68d56 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -3130,10 +3130,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { async findStripeSubscriptionId(ctx: TraceContext, attributionId: string): Promise { throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); } - async createOrUpdateStripeCustomerForTeam(ctx: TraceContext, teamId: string, currency: string): Promise { - throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); - } - async createOrUpdateStripeCustomerForUser(ctx: TraceContext, currency: string): Promise { + async createStripeCustomer(ctx: TraceContext, attributionId: string, currency: string): Promise { throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); } async subscribeToStripe(ctx: TraceContext, attributionId: string, setupIntentId: string): Promise { From d3e5716bad272af821f23c0b77ee766f33413b6b Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Fri, 23 Sep 2022 08:21:56 +0000 Subject: [PATCH 6/7] [server] Refactor StripeService.findCustomer and .findUncancelledSubscription to only use attributionIds --- .../ee/src/billing/billing-mode.spec.db.ts | 11 ++++----- .../server/ee/src/billing/billing-mode.ts | 24 +++++++++---------- .../ee/src/billing/entitlement-service-ubp.ts | 19 +++++++-------- .../server/ee/src/user/stripe-service.ts | 22 ++++++++--------- .../ee/src/workspace/gitpod-server-impl.ts | 18 +++++--------- components/server/src/user/user-service.ts | 12 ++++------ 6 files changed, 45 insertions(+), 61 deletions(-) diff --git a/components/server/ee/src/billing/billing-mode.spec.db.ts b/components/server/ee/src/billing/billing-mode.spec.db.ts index b4666a8a2d6394..50759e3421ff75 100644 --- a/components/server/ee/src/billing/billing-mode.spec.db.ts +++ b/components/server/ee/src/billing/billing-mode.spec.db.ts @@ -50,24 +50,21 @@ class StripeServiceMock extends StripeService { super(); } - async findUncancelledSubscriptionByCustomer(customerId: string): Promise { - if (this.subscription?.customer === customerId) { + async findUncancelledSubscriptionByAttributionId(attributionId: string): Promise { + const customerId = await this.findCustomerByAttributionId(attributionId); + if (!!customerId && this.subscription?.customer === customerId) { return this.subscription.id; } return undefined; } - async findCustomerByUserId(userId: string): Promise { + async findCustomerByAttributionId(attributionId: string): Promise { const customerId = this.subscription?.customer; if (!customerId) { return undefined; } return customerId; } - - async findCustomerByTeamId(teamId: string): Promise { - return this.findCustomerByUserId(teamId); - } } class ConfigCatClientMock implements Client { diff --git a/components/server/ee/src/billing/billing-mode.ts b/components/server/ee/src/billing/billing-mode.ts index 77e2d4b6d47f18..2b84d105d35054 100644 --- a/components/server/ee/src/billing/billing-mode.ts +++ b/components/server/ee/src/billing/billing-mode.ts @@ -41,7 +41,7 @@ export class BillingModesImpl implements BillingModes { @inject(Config) protected readonly config: Config; @inject(ConfigCatClientFactory) protected readonly configCatClientFactory: ConfigCatClientFactory; @inject(SubscriptionService) protected readonly subscriptionSvc: SubscriptionService; - @inject(StripeService) protected readonly stripeSvc: StripeService; + @inject(StripeService) protected readonly stripeService: StripeService; @inject(TeamSubscriptionDB) protected readonly teamSubscriptionDb: TeamSubscriptionDB; @inject(TeamSubscription2DB) protected readonly teamSubscription2Db: TeamSubscription2DB; @inject(TeamDB) protected readonly teamDB: TeamDB; @@ -116,12 +116,11 @@ export class BillingModesImpl implements BillingModes { // Stripe: Active personal subsciption? let hasUbbPersonal = false; - const customerId = await this.stripeSvc.findCustomerByUserId(user.id); - if (customerId) { - const subscriptionId = await this.stripeSvc.findUncancelledSubscriptionByCustomer(customerId); - if (subscriptionId) { - hasUbbPersonal = true; - } + const subscriptionId = await this.stripeService.findUncancelledSubscriptionByAttributionId( + AttributionId.render({ kind: "user", userId: user.id }), + ); + if (subscriptionId) { + hasUbbPersonal = true; } // 3. Check team memberships/plans @@ -192,12 +191,11 @@ export class BillingModesImpl implements BillingModes { // 3. Now we're usage-based. We only have to figure out whether we have a plan yet or not. const result: BillingMode = { mode: "usage-based" }; - const customerId = await this.stripeSvc.findCustomerByTeamId(team.id); - if (customerId) { - const subscriptionId = await this.stripeSvc.findUncancelledSubscriptionByCustomer(customerId); - if (subscriptionId) { - result.paid = true; - } + const subscriptionId = await this.stripeService.findUncancelledSubscriptionByAttributionId( + AttributionId.render({ kind: "team", teamId: team.id }), + ); + if (subscriptionId) { + result.paid = true; } return result; } diff --git a/components/server/ee/src/billing/entitlement-service-ubp.ts b/components/server/ee/src/billing/entitlement-service-ubp.ts index eca01be23f1b50..176fc7c8a60dcf 100644 --- a/components/server/ee/src/billing/entitlement-service-ubp.ts +++ b/components/server/ee/src/billing/entitlement-service-ubp.ts @@ -114,21 +114,18 @@ export class EntitlementServiceUBP implements EntitlementService { protected async hasPaidSubscription(user: User, date: Date): Promise { // Paid user? - const customerId = await this.stripeService.findCustomerByUserId(user.id); - if (customerId) { - const subscriptionId = await this.stripeService.findUncancelledSubscriptionByCustomer(customerId); - if (subscriptionId) { - return true; - } + const subscriptionId = await this.stripeService.findUncancelledSubscriptionByAttributionId( + AttributionId.render({ kind: "user", userId: user.id }), + ); + if (subscriptionId) { + return true; } // Member of paid team? const teams = await this.teamDB.findTeamsByUser(user.id); const isTeamSubscribedPromises = teams.map(async (team: Team) => { - const customerId = await this.stripeService.findCustomerByTeamId(team.id); - if (!customerId) { - return false; - } - const subscriptionId = await this.stripeService.findUncancelledSubscriptionByCustomer(customerId); + const subscriptionId = await this.stripeService.findUncancelledSubscriptionByAttributionId( + AttributionId.render({ kind: "team", teamId: team.id }), + ); return !!subscriptionId; }); // Return the first truthy promise, or false if all the promises were falsy. diff --git a/components/server/ee/src/user/stripe-service.ts b/components/server/ee/src/user/stripe-service.ts index 790f149eb3f7ba..a4215630971246 100644 --- a/components/server/ee/src/user/stripe-service.ts +++ b/components/server/ee/src/user/stripe-service.ts @@ -34,14 +34,6 @@ export class StripeService { return await this.getStripe().setupIntents.create({ usage: "on_session" }); } - async findCustomerByUserId(userId: string): Promise { - return this.findCustomerByAttributionId(AttributionId.render({ kind: "user", userId })); - } - - async findCustomerByTeamId(teamId: string): Promise { - return this.findCustomerByAttributionId(AttributionId.render({ kind: "team", teamId })); - } - async findCustomerByAttributionId(attributionId: string): Promise { const query = `metadata['attributionId']:'${attributionId}'`; const result = await this.getStripe().customers.search({ query }); @@ -119,7 +111,9 @@ export class StripeService { } async getPortalUrlForTeam(team: Team): Promise { - const customerId = await this.findCustomerByTeamId(team.id); + const customerId = await this.findCustomerByAttributionId( + AttributionId.render({ kind: "team", teamId: team.id }), + ); if (!customerId) { throw new Error(`No Stripe Customer ID found for team '${team.id}'`); } @@ -131,7 +125,9 @@ export class StripeService { } async getPortalUrlForUser(user: User): Promise { - const customerId = await this.findCustomerByUserId(user.id); + const customerId = await this.findCustomerByAttributionId( + AttributionId.render({ kind: "user", userId: user.id }), + ); if (!customerId) { throw new Error(`No Stripe Customer ID found for user '${user.id}'`); } @@ -142,7 +138,11 @@ export class StripeService { return session.url; } - async findUncancelledSubscriptionByCustomer(customerId: string): Promise { + async findUncancelledSubscriptionByAttributionId(attributionId: string): Promise { + const customerId = await this.findCustomerByAttributionId(attributionId); + if (!customerId) { + return undefined; + } const result = await this.getStripe().subscriptions.list({ customer: customerId, }); diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 10b7a764209421..114d356c034c0a 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -1596,12 +1596,11 @@ export class GitpodServerEEImpl extends GitpodServerImpl { { teamId, chargebeeSubscriptionId }, ); } - const teamCustomerId = await this.stripeService.findCustomerByTeamId(teamId); - if (teamCustomerId) { - const subsciptionId = await this.stripeService.findUncancelledSubscriptionByCustomer(teamCustomerId); - if (subsciptionId) { - await this.stripeService.cancelSubscription(subsciptionId); - } + const subscriptionId = await this.stripeService.findUncancelledSubscriptionByAttributionId( + AttributionId.render({ kind: "team", teamId: teamId }), + ); + if (subscriptionId) { + await this.stripeService.cancelSubscription(subscriptionId); } } @@ -2066,12 +2065,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { if (attrId.kind == "team") { await this.guardTeamOperation(attrId.teamId, "get"); } - const customerId = await this.stripeService.findCustomerByAttributionId(attributionId); - if (!customerId) { - return undefined; - } - - const subscriptionId = await this.stripeService.findUncancelledSubscriptionByCustomer(customerId); + const subscriptionId = await this.stripeService.findUncancelledSubscriptionByAttributionId(attributionId); return subscriptionId; } catch (error) { log.error(`Failed to get Stripe Subscription ID for '${attributionId}'`, error); diff --git a/components/server/src/user/user-service.ts b/components/server/src/user/user-service.ts index 165cd8dd2338de..73ea77c7e40a6c 100644 --- a/components/server/src/user/user-service.ts +++ b/components/server/src/user/user-service.ts @@ -196,11 +196,9 @@ export class UserService { } protected async findTeamUsageBasedSubscriptionId(team: Team): Promise { - const customerId = await this.stripeService.findCustomerByTeamId(team.id); - if (!customerId) { - return; - } - const subscriptionId = await this.stripeService.findUncancelledSubscriptionByCustomer(customerId); + const subscriptionId = await this.stripeService.findUncancelledSubscriptionByAttributionId( + AttributionId.render({ kind: "team", teamId: team.id }), + ); return subscriptionId; } @@ -325,8 +323,8 @@ export class UserService { if (!membership) { throw new Error("Cannot attribute to an unrelated team."); } - const teamCustomer = await this.stripeService.findCustomerByTeamId(attributionId.teamId); - if (!teamCustomer) { + const teamCustomerId = await this.stripeService.findCustomerByAttributionId(usageAttributionId); + if (!teamCustomerId) { throw new Error("Cannot attribute to team without Stripe customer."); } user.usageAttributionId = usageAttributionId; From ad8d0acbc3e95cc93d2f4d09636d997d0562bd65 Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Fri, 23 Sep 2022 08:33:56 +0000 Subject: [PATCH 7/7] [server] Refactor StripeService.createCustomer to only use attributionIds --- .../components/UsageBasedBillingConfig.tsx | 2 +- .../gitpod-protocol/src/gitpod-service.ts | 2 +- .../server/ee/src/user/stripe-service.ts | 45 +++++-------------- .../ee/src/workspace/gitpod-server-impl.ts | 21 +++++---- components/server/src/auth/rate-limiter.ts | 2 +- .../src/workspace/gitpod-server-impl.ts | 2 +- 6 files changed, 25 insertions(+), 49 deletions(-) diff --git a/components/dashboard/src/components/UsageBasedBillingConfig.tsx b/components/dashboard/src/components/UsageBasedBillingConfig.tsx index d5a41053e80175..53980dc1962288 100644 --- a/components/dashboard/src/components/UsageBasedBillingConfig.tsx +++ b/components/dashboard/src/components/UsageBasedBillingConfig.tsx @@ -296,7 +296,7 @@ function CreditCardInputForm(props: { attributionId: string }) { setIsLoading(true); try { // Create Stripe customer with currency - await getGitpodService().server.createStripeCustomer(props.attributionId, currency); + await getGitpodService().server.createStripeCustomerIfNeeded(props.attributionId, currency); const result = await stripe.confirmSetup({ elements, confirmParams: { diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index f0cff6489a7853..b7e78d0e7bc45b 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -287,7 +287,7 @@ export interface GitpodServer extends JsonRpcServer, AdminServer, getStripePublishableKey(): Promise; getStripeSetupIntentClientSecret(): Promise; findStripeSubscriptionId(attributionId: string): Promise; - createStripeCustomer(attributionId: string, currency: string): Promise; + createStripeCustomerIfNeeded(attributionId: string, currency: string): Promise; subscribeToStripe(attributionId: string, setupIntentId: string): Promise; getStripePortalUrl(attributionId: string): Promise; getUsageLimit(attributionId: string): Promise; diff --git a/components/server/ee/src/user/stripe-service.ts b/components/server/ee/src/user/stripe-service.ts index a4215630971246..3a5135afea4793 100644 --- a/components/server/ee/src/user/stripe-service.ts +++ b/components/server/ee/src/user/stripe-service.ts @@ -43,55 +43,32 @@ export class StripeService { return result.data[0]?.id; } - async createCustomerForUser(user: User): Promise { - const attributionId = AttributionId.render({ kind: "user", userId: user.id }); + async createCustomerForAttributionId( + attributionId: string, + preferredCurrency: string, + billingEmail?: string, + billingName?: string, + ): Promise { if (await this.findCustomerByAttributionId(attributionId)) { - throw new Error(`A Stripe customer already exists for user '${user.id}'`); + throw new Error(`A Stripe customer already exists for '${attributionId}'`); } // Create the customer in Stripe const customer = await this.getStripe().customers.create({ - email: User.getPrimaryEmail(user), - name: User.getName(user), - metadata: { attributionId }, + email: billingEmail, + name: billingName, + metadata: { attributionId, preferredCurrency }, }); // Wait for the customer to show up in Stripe search results before proceeding let attempts = 0; while (!(await this.findCustomerByAttributionId(attributionId))) { await new Promise((resolve) => setTimeout(resolve, POLL_CREATED_CUSTOMER_INTERVAL_MS)); if (++attempts > POLL_CREATED_CUSTOMER_MAX_ATTEMPTS) { - throw new Error(`Could not confirm Stripe customer creation for user '${user.id}'`); + throw new Error(`Could not confirm Stripe customer creation for '${attributionId}'`); } } return customer.id; } - async createCustomerForTeam(user: User, team: Team): Promise { - const attributionId = AttributionId.render({ kind: "team", teamId: team.id }); - if (await this.findCustomerByAttributionId(attributionId)) { - throw new Error(`A Stripe customer already exists for team '${team.id}'`); - } - // Create the customer in Stripe - const userName = User.getName(user); - const customer = await this.getStripe().customers.create({ - email: User.getPrimaryEmail(user), - name: userName ? `${userName} (${team.name})` : team.name, - metadata: { attributionId }, - }); - // Wait for the customer to show up in Stripe search results before proceeding - let attempts = 0; - while (!(await this.findCustomerByAttributionId(attributionId))) { - await new Promise((resolve) => setTimeout(resolve, POLL_CREATED_CUSTOMER_INTERVAL_MS)); - if (++attempts > POLL_CREATED_CUSTOMER_MAX_ATTEMPTS) { - throw new Error(`Could not confirm Stripe customer creation for team '${team.id}'`); - } - } - return customer.id; - } - - async setPreferredCurrencyForCustomer(customerId: string, currency: string): Promise { - await this.getStripe().customers.update(customerId, { metadata: { preferredCurrency: currency } }); - } - async setDefaultPaymentMethodForCustomer(customerId: string, setupIntentId: string): Promise { const setupIntent = await this.getStripe().setupIntents.retrieve(setupIntentId); if (typeof setupIntent.payment_method !== "string") { diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 114d356c034c0a..b6bbf50231f124 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -2076,8 +2076,8 @@ export class GitpodServerEEImpl extends GitpodServerImpl { } } - async createStripeCustomer(ctx: TraceContext, attributionId: string, currency: string): Promise { - const user = this.checkAndBlockUser("createStripeCustomer"); + async createStripeCustomerIfNeeded(ctx: TraceContext, attributionId: string, currency: string): Promise { + const user = this.checkAndBlockUser("createStripeCustomerIfNeeded"); const attrId = AttributionId.parse(attributionId); if (!attrId) { throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attributionId '${attributionId}'`); @@ -2096,17 +2096,16 @@ export class GitpodServerEEImpl extends GitpodServerImpl { await this.ensureStripeApiIsAllowed({ user }); } try { - if (await this.stripeService.findCustomerByAttributionId(attributionId)) { - throw new ResponseError( - ErrorCodes.BAD_REQUEST, - "A Stripe customer profile already exists for this attributionId", + if (!(await this.stripeService.findCustomerByAttributionId(attributionId))) { + const billingEmail = User.getPrimaryEmail(user); + const billingName = attrId.kind === "team" ? team!.name : User.getName(user); + await this.stripeService.createCustomerForAttributionId( + attributionId, + currency, + billingEmail, + billingName, ); } - const customerId = - attrId.kind === "team" - ? await this.stripeService.createCustomerForTeam(user, team!) - : await this.stripeService.createCustomerForUser(user); - await this.stripeService.setPreferredCurrencyForCustomer(customerId, currency); } catch (error) { log.error(`Failed to create Stripe customer profile for '${attributionId}'`, error); throw new ResponseError( diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index 3846f90dbf0862..d739b4035e6df0 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -201,7 +201,7 @@ const defaultFunctions: FunctionsConfig = { getStripePublishableKey: { group: "default", points: 1 }, getStripeSetupIntentClientSecret: { group: "default", points: 1 }, findStripeSubscriptionId: { group: "default", points: 1 }, - createStripeCustomer: { group: "default", points: 1 }, + createStripeCustomerIfNeeded: { group: "default", points: 1 }, subscribeToStripe: { group: "default", points: 1 }, getStripePortalUrl: { group: "default", points: 1 }, listUsage: { group: "default", points: 1 }, diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index fc4e9c84b68d56..87b3b3e75d1c33 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -3130,7 +3130,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { async findStripeSubscriptionId(ctx: TraceContext, attributionId: string): Promise { throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); } - async createStripeCustomer(ctx: TraceContext, attributionId: string, currency: string): Promise { + async createStripeCustomerIfNeeded(ctx: TraceContext, attributionId: string, currency: string): Promise { throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); } async subscribeToStripe(ctx: TraceContext, attributionId: string, setupIntentId: string): Promise {