diff --git a/components/ee/payment-endpoint/BUILD.yaml b/components/ee/payment-endpoint/BUILD.yaml index 387a69117dff32..24af0d4afd5a11 100644 --- a/components/ee/payment-endpoint/BUILD.yaml +++ b/components/ee/payment-endpoint/BUILD.yaml @@ -10,6 +10,7 @@ packages: deps: - components/gitpod-db:lib - components/gitpod-protocol:lib + - components/usage-api/typescript:lib config: packaging: offline-mirror yarnLock: ${coreYarnLockBase}/../yarn.lock @@ -24,6 +25,7 @@ packages: deps: - components/gitpod-db:lib - components/gitpod-protocol:lib + - components/usage-api/typescript:lib - :dbtest config: packaging: library @@ -54,6 +56,7 @@ packages: - components/gitpod-db:dbtest-init - components/gitpod-db:lib - components/gitpod-protocol:lib + - components/usage-api/typescript:lib config: packaging: library yarnLock: ${coreYarnLockBase}/../yarn.lock diff --git a/components/ee/payment-endpoint/package.json b/components/ee/payment-endpoint/package.json index e9ef2e2fda6d6d..a0c475bc66ef00 100644 --- a/components/ee/payment-endpoint/package.json +++ b/components/ee/payment-endpoint/package.json @@ -7,6 +7,7 @@ "dependencies": { "@gitpod/gitpod-db": "0.1.5", "@gitpod/gitpod-protocol": "0.1.5", + "@gitpod/usage-api": "0.1.5", "@octokit/rest": "18.5.6", "@octokit/webhooks": "9.17.0", "body-parser": "^1.19.2", diff --git a/components/ee/payment-endpoint/src/chargebee/subscription-handler.ts b/components/ee/payment-endpoint/src/chargebee/subscription-handler.ts index 7cd7d213b26bf3..9075d44682196c 100644 --- a/components/ee/payment-endpoint/src/chargebee/subscription-handler.ts +++ b/components/ee/payment-endpoint/src/chargebee/subscription-handler.ts @@ -4,30 +4,30 @@ * See License.enterprise.txt in the project root folder. */ -import { inject, injectable } from 'inversify'; - -import { SubscriptionService } from '../accounting/subscription-service'; -import { AccountingDB } from '@gitpod/gitpod-db/lib/accounting-db'; -import { log, LogContext } from '@gitpod/gitpod-protocol/lib/util/logging'; -import { SubscriptionMapperFactory } from './subscription-mapper'; -import { Plans } from '@gitpod/gitpod-protocol/lib/plans'; -import { Chargebee as chargebee } from './chargebee-types'; -import { EventHandler } from './chargebee-event-handler'; -import { UpgradeHelper } from './upgrade-helper'; +import { inject, injectable } from "inversify"; + +import { SubscriptionService } from "../accounting/subscription-service"; +import { AccountingDB } from "@gitpod/gitpod-db/lib/accounting-db"; +import { log, LogContext } from "@gitpod/gitpod-protocol/lib/util/logging"; +import { SubscriptionMapper } from "./subscription-mapper"; +import { Plans } from "@gitpod/gitpod-protocol/lib/plans"; +import { Chargebee as chargebee } from "./chargebee-types"; +import { EventHandler } from "./chargebee-event-handler"; +import { UpgradeHelper } from "./upgrade-helper"; import { formatDate } from "@gitpod/gitpod-protocol/lib/util/date-time"; -import { getUpdatedAt } from './chargebee-subscription-helper'; -import { UserPaidSubscription } from '@gitpod/gitpod-protocol/lib/accounting-protocol'; -import { DBSubscriptionAdditionalData } from '@gitpod/gitpod-db/lib/typeorm/entity/db-subscription'; +import { getUpdatedAt } from "./chargebee-subscription-helper"; +import { UserPaidSubscription } from "@gitpod/gitpod-protocol/lib/accounting-protocol"; +import { DBSubscriptionAdditionalData } from "@gitpod/gitpod-db/lib/typeorm/entity/db-subscription"; @injectable() export class SubscriptionHandler implements EventHandler { @inject(SubscriptionService) protected readonly subscriptionService: SubscriptionService; @inject(AccountingDB) protected readonly db: AccountingDB; - @inject(SubscriptionMapperFactory) protected readonly mapperFactory: SubscriptionMapperFactory; + @inject(SubscriptionMapper) protected readonly mapper: SubscriptionMapper; @inject(UpgradeHelper) protected readonly upgradeHelper: UpgradeHelper; canHandle(event: chargebee.Event): boolean { - if (event.event_type.startsWith('subscription')) { + if (event.event_type.startsWith("subscription")) { const evt = event as chargebee.Event; const plan = Plans.getById(evt.content.subscription.plan_id); return !!plan && !plan.team; @@ -44,16 +44,16 @@ export class SubscriptionHandler implements EventHandler oldPlan.pricePerMonth) { // Upgrade: Charge for it! - const diffInCents = (newPlan.pricePerMonth * 100) - (oldPlan.pricePerMonth * 100); + const diffInCents = newPlan.pricePerMonth * 100 - oldPlan.pricePerMonth * 100; const upgradeTimestamp = getUpdatedAt(chargebeeSubscription); - const description = `Difference on Upgrade from '${oldPlan.name}' to '${newPlan.name}' (${formatDate(upgradeTimestamp)})`; - await this.upgradeHelper.chargeForUpgrade(userId, chargebeeSubscription.id, diffInCents, description, upgradeTimestamp); + const description = `Difference on Upgrade from '${oldPlan.name}' to '${newPlan.name}' (${formatDate( + upgradeTimestamp, + )})`; + await this.upgradeHelper.chargeForUpgrade( + userId, + chargebeeSubscription.id, + diffInCents, + description, + upgradeTimestamp, + ); } } @@ -123,14 +134,13 @@ export class SubscriptionHandler implements EventHandler) { await this.db.transaction(async (db) => { const subscriptions = await db.findAllSubscriptionsForUser(userId); - const userPaidSubscriptions = subscriptions.filter(s => UserPaidSubscription.is(s)); + const userPaidSubscriptions = subscriptions.filter((s) => UserPaidSubscription.is(s)); - const mapper = this.mapperFactory.newMapper(); - const delta = mapper.map(userPaidSubscriptions, event).getResult(); + const delta = this.mapper.map(userPaidSubscriptions, event).getResult(); await Promise.all([ - ...delta.updates.map(s => db.storeSubscription(s)), - ...delta.inserts.map(s => db.newSubscription(s)) + ...delta.updates.map((s) => db.storeSubscription(s)), + ...delta.inserts.map((s) => db.newSubscription(s)), ]); }); } @@ -142,4 +152,4 @@ export class SubscriptionHandler implements EventHandler(SubscriptionMapperFactory); - mapper: SubscriptionMapper; - - async before() { - this.mapper = this.mapperFactory.newMapper(); - } +@suite +class SubscriptionMapperSpec { + mapper = testContainer.get(SubscriptionMapper); @test async first_basic() { const userCreatedDate = new Date("2018-11-24T04:05:48.000Z").toISOString(); - const events = loadFromFolder('first_basic'); + const events = loadFromFolder("first_basic"); let subscriptions: Subscription[] = []; - const basicCreated = events.get('basic-created')!; + const basicCreated = events.get("basic-created")!; const model = this.mapper.map(filterFree(subscriptions), basicCreated); subscriptions = model.mergedWithFreeSubscriptions(userCreatedDate); - expect(filterIrrelevantFields(subscriptions)).to.deep.equal([{ - userId: '68b75ca3-df45-4fde-8665-9f9f3d749d55', - startDate: "2018-11-29T12:01:09.000Z", - amount: 100, - planId: "basic-usd", - paymentReference: "Hr55127RAn4dqQ1e7x" - }, - { - userId: '68b75ca3-df45-4fde-8665-9f9f3d749d55', - startDate: "2018-11-24T04:05:48.000Z", - endDate: "2018-11-29T12:01:09.000Z", - amount: 100, - planId: "free", - cancellationDate: '2018-11-29T12:01:09.000Z' - }]); + expect(filterIrrelevantFields(subscriptions)).to.deep.equal([ + { + userId: "68b75ca3-df45-4fde-8665-9f9f3d749d55", + startDate: "2018-11-29T12:01:09.000Z", + amount: 100, + planId: "basic-usd", + paymentReference: "Hr55127RAn4dqQ1e7x", + }, + { + userId: "68b75ca3-df45-4fde-8665-9f9f3d749d55", + startDate: "2018-11-24T04:05:48.000Z", + endDate: "2018-11-29T12:01:09.000Z", + amount: 100, + planId: "free", + cancellationDate: "2018-11-29T12:01:09.000Z", + }, + ]); } @test async full_cycle() { const userCreatedDate = new Date("2018-11-20T15:25:48.000Z").toISOString(); - const events = loadFromFolder('full_cycle'); + const events = loadFromFolder("full_cycle"); let subscriptions: Subscription[] = []; - const basicCreated = events.get('basic-created')!; + const basicCreated = events.get("basic-created")!; let model = this.mapper.map(filterFree(subscriptions), basicCreated); subscriptions = model.mergedWithFreeSubscriptions(userCreatedDate); - expect(filterIrrelevantFields(subscriptions), 'basic-created').to.deep.equal([{ - userId: '31376076-3362-4faa-9012-01ee684b73ff', - startDate: '2018-11-27T15:25:48.000Z', - planId: 'basic-usd', - amount: 100, - paymentReference: 'Hr5511ERAcD8VHCmF' - }, - { - userId: '31376076-3362-4faa-9012-01ee684b73ff', - planId: 'free', - amount: 100, - startDate: '2018-11-20T15:25:48.000Z', - endDate: '2018-11-27T15:25:48.000Z', - cancellationDate: '2018-11-27T15:25:48.000Z' - }]); + expect(filterIrrelevantFields(subscriptions), "basic-created").to.deep.equal([ + { + userId: "31376076-3362-4faa-9012-01ee684b73ff", + startDate: "2018-11-27T15:25:48.000Z", + planId: "basic-usd", + amount: 100, + paymentReference: "Hr5511ERAcD8VHCmF", + }, + { + userId: "31376076-3362-4faa-9012-01ee684b73ff", + planId: "free", + amount: 100, + startDate: "2018-11-20T15:25:48.000Z", + endDate: "2018-11-27T15:25:48.000Z", + cancellationDate: "2018-11-27T15:25:48.000Z", + }, + ]); - const upgradeToPro = events.get('upgrade-to-pro')!; + const upgradeToPro = events.get("upgrade-to-pro")!; model = this.mapper.map(filterFree(subscriptions), upgradeToPro); subscriptions = model.mergedWithFreeSubscriptions(userCreatedDate); - expect(filterIrrelevantFields(subscriptions), 'upgrade-to-pro').to.deep.equal([{ - userId: '31376076-3362-4faa-9012-01ee684b73ff', - startDate: '2018-11-27T15:25:48.000Z', - planId: 'professional-usd', - amount: ABSOLUTE_MAX_USAGE, - paymentReference: 'Hr5511ERAcD8VHCmF' - }, - { - userId: '31376076-3362-4faa-9012-01ee684b73ff', - planId: 'free', - amount: 100, - startDate: '2018-11-20T15:25:48.000Z', - endDate: '2018-11-27T15:25:48.000Z', - cancellationDate: '2018-11-27T15:25:48.000Z' - }]); + expect(filterIrrelevantFields(subscriptions), "upgrade-to-pro").to.deep.equal([ + { + userId: "31376076-3362-4faa-9012-01ee684b73ff", + startDate: "2018-11-27T15:25:48.000Z", + planId: "professional-usd", + amount: ABSOLUTE_MAX_USAGE, + paymentReference: "Hr5511ERAcD8VHCmF", + }, + { + userId: "31376076-3362-4faa-9012-01ee684b73ff", + planId: "free", + amount: 100, + startDate: "2018-11-20T15:25:48.000Z", + endDate: "2018-11-27T15:25:48.000Z", + cancellationDate: "2018-11-27T15:25:48.000Z", + }, + ]); - const proCancelled = events.get('pro-cancelled')!; + const proCancelled = events.get("pro-cancelled")!; model = this.mapper.map(filterFree(subscriptions), proCancelled); subscriptions = model.mergedWithFreeSubscriptions(userCreatedDate); - expect(filterIrrelevantFields(subscriptions), 'pro-cancelled').to.deep.equal([{ - userId: '31376076-3362-4faa-9012-01ee684b73ff', - startDate: '2018-12-27T15:25:48.000Z', - planId: 'free', - amount: 100 - }, - { - userId: '31376076-3362-4faa-9012-01ee684b73ff', - startDate: '2018-11-27T15:25:48.000Z', - planId: 'professional-usd', - amount: ABSOLUTE_MAX_USAGE, - paymentReference: 'Hr5511ERAcD8VHCmF', - endDate: '2018-12-27T15:25:48.000Z', - cancellationDate: '2018-11-27T15:56:23.000Z' - }, - { - userId: '31376076-3362-4faa-9012-01ee684b73ff', - planId: 'free', - amount: 100, - startDate: '2018-11-20T15:25:48.000Z', - endDate: '2018-11-27T15:25:48.000Z', - cancellationDate: '2018-11-27T15:25:48.000Z' - }]); + expect(filterIrrelevantFields(subscriptions), "pro-cancelled").to.deep.equal([ + { + userId: "31376076-3362-4faa-9012-01ee684b73ff", + startDate: "2018-12-27T15:25:48.000Z", + planId: "free", + amount: 100, + }, + { + userId: "31376076-3362-4faa-9012-01ee684b73ff", + startDate: "2018-11-27T15:25:48.000Z", + planId: "professional-usd", + amount: ABSOLUTE_MAX_USAGE, + paymentReference: "Hr5511ERAcD8VHCmF", + endDate: "2018-12-27T15:25:48.000Z", + cancellationDate: "2018-11-27T15:56:23.000Z", + }, + { + userId: "31376076-3362-4faa-9012-01ee684b73ff", + planId: "free", + amount: 100, + startDate: "2018-11-20T15:25:48.000Z", + endDate: "2018-11-27T15:25:48.000Z", + cancellationDate: "2018-11-27T15:25:48.000Z", + }, + ]); - const proReactivated = events.get('pro-reactivated')!; + const proReactivated = events.get("pro-reactivated")!; model = this.mapper.map(filterFree(subscriptions), proReactivated); subscriptions = model.mergedWithFreeSubscriptions(userCreatedDate); - expect(filterIrrelevantFields(subscriptions), 'pro-reactivated').to.deep.equal([{ - amount: ABSOLUTE_MAX_USAGE, - paymentReference: 'Hr5511ERAcD8VHCmF', - planId: 'professional-usd', - startDate: '2018-11-27T15:25:48.000Z', - userId: '31376076-3362-4faa-9012-01ee684b73ff' - }, - { - userId: '31376076-3362-4faa-9012-01ee684b73ff', - startDate: '2018-11-27T15:25:48.000Z', - planId: 'professional-usd', - amount: ABSOLUTE_MAX_USAGE, - paymentReference: 'Hr5511ERAcD8VHCmF', - endDate: '2018-12-27T15:25:48.000Z', - cancellationDate: '2018-11-27T15:56:23.000Z' - }, - { - userId: '31376076-3362-4faa-9012-01ee684b73ff', - planId: 'free', - amount: 100, - startDate: '2018-11-20T15:25:48.000Z', - endDate: '2018-11-27T15:25:48.000Z', - cancellationDate: '2018-11-27T15:25:48.000Z' - }]); + expect(filterIrrelevantFields(subscriptions), "pro-reactivated").to.deep.equal([ + { + amount: ABSOLUTE_MAX_USAGE, + paymentReference: "Hr5511ERAcD8VHCmF", + planId: "professional-usd", + startDate: "2018-11-27T15:25:48.000Z", + userId: "31376076-3362-4faa-9012-01ee684b73ff", + }, + { + userId: "31376076-3362-4faa-9012-01ee684b73ff", + startDate: "2018-11-27T15:25:48.000Z", + planId: "professional-usd", + amount: ABSOLUTE_MAX_USAGE, + paymentReference: "Hr5511ERAcD8VHCmF", + endDate: "2018-12-27T15:25:48.000Z", + cancellationDate: "2018-11-27T15:56:23.000Z", + }, + { + userId: "31376076-3362-4faa-9012-01ee684b73ff", + planId: "free", + amount: 100, + startDate: "2018-11-20T15:25:48.000Z", + endDate: "2018-11-27T15:25:48.000Z", + cancellationDate: "2018-11-27T15:25:48.000Z", + }, + ]); - const downgradeToBasic = events.get('downgrade-to-basic')!; + const downgradeToBasic = events.get("downgrade-to-basic")!; model = this.mapper.map(filterFree(subscriptions), downgradeToBasic); subscriptions = model.mergedWithFreeSubscriptions(userCreatedDate); - expect(filterIrrelevantFields(subscriptions), 'downgrade-to-basic').to.deep.equal([{ - amount: ABSOLUTE_MAX_USAGE, - paymentReference: 'Hr5511ERAcD8VHCmF', - planId: 'professional-usd', - startDate: '2018-11-27T15:25:48.000Z', - userId: '31376076-3362-4faa-9012-01ee684b73ff' - }, - { - userId: '31376076-3362-4faa-9012-01ee684b73ff', - startDate: '2018-11-27T15:25:48.000Z', - planId: 'professional-usd', - amount: ABSOLUTE_MAX_USAGE, - paymentReference: 'Hr5511ERAcD8VHCmF', - endDate: '2018-12-27T15:25:48.000Z', - cancellationDate: '2018-11-27T15:56:23.000Z' - }, - { - userId: '31376076-3362-4faa-9012-01ee684b73ff', - planId: 'free', - amount: 100, - startDate: '2018-11-20T15:25:48.000Z', - endDate: '2018-11-27T15:25:48.000Z', - cancellationDate: '2018-11-27T15:25:48.000Z' - }]); + expect(filterIrrelevantFields(subscriptions), "downgrade-to-basic").to.deep.equal([ + { + amount: ABSOLUTE_MAX_USAGE, + paymentReference: "Hr5511ERAcD8VHCmF", + planId: "professional-usd", + startDate: "2018-11-27T15:25:48.000Z", + userId: "31376076-3362-4faa-9012-01ee684b73ff", + }, + { + userId: "31376076-3362-4faa-9012-01ee684b73ff", + startDate: "2018-11-27T15:25:48.000Z", + planId: "professional-usd", + amount: ABSOLUTE_MAX_USAGE, + paymentReference: "Hr5511ERAcD8VHCmF", + endDate: "2018-12-27T15:25:48.000Z", + cancellationDate: "2018-11-27T15:56:23.000Z", + }, + { + userId: "31376076-3362-4faa-9012-01ee684b73ff", + planId: "free", + amount: 100, + startDate: "2018-11-20T15:25:48.000Z", + endDate: "2018-11-27T15:25:48.000Z", + cancellationDate: "2018-11-27T15:25:48.000Z", + }, + ]); } - @test async downgrade_unlimited_to_professional_new() { const userCreatedDate = new Date("2018-11-20T15:25:48.000Z").toISOString(); - const events = loadFromFolder('downgrade_unlimited_to_professional_new'); + const events = loadFromFolder("downgrade_unlimited_to_professional_new"); let subscriptions: Subscription[] = [ - { - userId: '643ac637-74ae-4f82-9a86-c8527eb1e496', - startDate: "2018-11-20T15:25:48.000Z", - endDate: "2019-04-07T13:17:58.000Z", - cancellationDate: '2019-04-07T13:17:58.000Z', - amount: 100, - uid: '7f76687f-8b09-482b-b059-5779e51c3a44', - planId: "free" - }, - { - userId: '643ac637-74ae-4f82-9a86-c8527eb1e496', - startDate: '2019-04-07T13:17:58.000Z', - amount: 5952, - uid: '7f76687f-8b09-482b-b059-5779e51c3a22', - planId: 'professional-eur', - paymentReference: '1mkVvmiRMxfTZj1JGs', - paymentData: { - downgradeDate: "2020-01-07T13:17:58.000Z" - } - }]; + { + userId: "643ac637-74ae-4f82-9a86-c8527eb1e496", + startDate: "2018-11-20T15:25:48.000Z", + endDate: "2019-04-07T13:17:58.000Z", + cancellationDate: "2019-04-07T13:17:58.000Z", + amount: 100, + uid: "7f76687f-8b09-482b-b059-5779e51c3a44", + planId: "free", + }, + { + userId: "643ac637-74ae-4f82-9a86-c8527eb1e496", + startDate: "2019-04-07T13:17:58.000Z", + amount: 5952, + uid: "7f76687f-8b09-482b-b059-5779e51c3a22", + planId: "professional-eur", + paymentReference: "1mkVvmiRMxfTZj1JGs", + paymentData: { + downgradeDate: "2020-01-07T13:17:58.000Z", + }, + }, + ]; - const basicCreated = events.get('downgrade-change-actual')!; + const basicCreated = events.get("downgrade-change-actual")!; let model = this.mapper.map(filterFree(subscriptions), basicCreated); subscriptions = model.mergedWithFreeSubscriptions(userCreatedDate); - expect(filterIrrelevantFields(subscriptions), 'downgrade-change-actual').to.deep.equal([ - { - userId: '643ac637-74ae-4f82-9a86-c8527eb1e496', - startDate: '2020-01-07T13:17:58.000Z', - amount: 11904, - planId: 'professional-new-eur', - paymentReference: '1mkVvmiRMxfTZj1JGs' - }, - { - userId: '643ac637-74ae-4f82-9a86-c8527eb1e496', - startDate: '2019-04-07T13:17:58.000Z', - endDate: '2020-01-07T13:17:58.000Z', - cancellationDate: '2020-01-07T13:17:58.000Z', - amount: 5952, - planId: 'professional-eur', - paymentReference: '1mkVvmiRMxfTZj1JGs', - paymentData: { - downgradeDate: "2020-01-07T13:17:58.000Z" - } - }, - { - userId: '643ac637-74ae-4f82-9a86-c8527eb1e496', - startDate: "2018-11-20T15:25:48.000Z", - endDate: "2019-04-07T13:17:58.000Z", - amount: 100, - planId: "free", - cancellationDate: '2019-04-07T13:17:58.000Z' - }]); + expect(filterIrrelevantFields(subscriptions), "downgrade-change-actual").to.deep.equal([ + { + userId: "643ac637-74ae-4f82-9a86-c8527eb1e496", + startDate: "2020-01-07T13:17:58.000Z", + amount: 11904, + planId: "professional-new-eur", + paymentReference: "1mkVvmiRMxfTZj1JGs", + }, + { + userId: "643ac637-74ae-4f82-9a86-c8527eb1e496", + startDate: "2019-04-07T13:17:58.000Z", + endDate: "2020-01-07T13:17:58.000Z", + cancellationDate: "2020-01-07T13:17:58.000Z", + amount: 5952, + planId: "professional-eur", + paymentReference: "1mkVvmiRMxfTZj1JGs", + paymentData: { + downgradeDate: "2020-01-07T13:17:58.000Z", + }, + }, + { + userId: "643ac637-74ae-4f82-9a86-c8527eb1e496", + startDate: "2018-11-20T15:25:48.000Z", + endDate: "2019-04-07T13:17:58.000Z", + amount: 100, + planId: "free", + cancellationDate: "2019-04-07T13:17:58.000Z", + }, + ]); } } const filterFree = (subscriptions: Subscription[]): Subscription[] => { - return subscriptions.filter(s => - s.planId !== Plans.FREE.chargebeeId - && s.planId !== Plans.FREE_50.chargebeeId - && s.planId !== Plans.FREE_OPEN_SOURCE.chargebeeId); + return subscriptions.filter( + (s) => + s.planId !== Plans.FREE.chargebeeId && + s.planId !== Plans.FREE_50.chargebeeId && + s.planId !== Plans.FREE_OPEN_SOURCE.chargebeeId, + ); }; const filterIrrelevantFields = (subscriptions: Subscription[]): Subscription[] => { - return subscriptions.map(s => { const r = { ...s }; delete (r as any).uid; return r; }); + return subscriptions.map((s) => { + const r = { ...s }; + delete (r as any).uid; + return r; + }); }; /** @@ -267,13 +290,13 @@ const filterIrrelevantFields = (subscriptions: Subscription[]): Subscription[] = */ const loadFromFolder = (testname: string): Map> => { const events: Map> = new Map(); - const testBasePath = path.join(__dirname, '../../test/fixtures/', testname); + const testBasePath = path.join(__dirname, "../../test/fixtures/", testname); const files = fs.readdirSync(testBasePath).sort(); for (const file of files) { const content = fs.readFileSync(path.join(testBasePath, file)); - const fileParts = file.split('.'); + const fileParts = file.split("."); if (fileParts.length < 2) { - throw new Error('Expected file name of the format ..[...]'); + throw new Error("Expected file name of the format ..[...]"); } const name = fileParts[1]; events.set(name, JSON.parse(content.toString())); @@ -281,11 +304,24 @@ const loadFromFolder = (testname: string): Map> => return events; }; -testContainer.bind(SubscriptionMapperFactory).toDynamicValue(ctx => { - return { - newMapper: () => { - return new SubscriptionMapper(); - } - }; -}).inSingletonScope(); +testContainer.bind(UsageServiceDefinition.name).toConstantValue({ + async getCostCenter(request: DeepPartial): Promise { + return { + costCenter: { + attributionId: "user:foo", + billingCycleStart: new Date(), + billingStrategy: CostCenter_BillingStrategy.BILLING_STRATEGY_OTHER, + spendingLimit: 500, + nextBillingTime: new Date(), + }, + }; + }, + /** SetCostCenter stores the given cost center */ + async setCostCenter(request: DeepPartial): Promise { + return { + costCenter: request!.costCenter as any, + }; + }, +}); +testContainer.bind(SubscriptionMapper).toSelf().inSingletonScope(); module.exports = new SubscriptionMapperSpec(); diff --git a/components/ee/payment-endpoint/src/chargebee/subscription-mapper.ts b/components/ee/payment-endpoint/src/chargebee/subscription-mapper.ts index 7d7c6e6cb12244..d8000a6969750c 100644 --- a/components/ee/payment-endpoint/src/chargebee/subscription-mapper.ts +++ b/components/ee/payment-endpoint/src/chargebee/subscription-mapper.ts @@ -7,12 +7,19 @@ import { Plans } from "@gitpod/gitpod-protocol/lib/plans"; import { orderByEndDateDescThenStartDateDesc } from "../accounting/accounting-util"; import { Subscription } from "@gitpod/gitpod-protocol/lib/accounting-protocol"; -import { oneMonthLater, secondsBefore } from '@gitpod/gitpod-protocol/lib/util/timeutil'; -import { Chargebee as chargebee } from './/chargebee-types'; +import { oneMonthLater, secondsBefore } from "@gitpod/gitpod-protocol/lib/util/timeutil"; +import { Chargebee as chargebee } from ".//chargebee-types"; -import { getStartDate, getCancelledAt, getUpdatedAt } from './chargebee-subscription-helper'; -import { SubscriptionModel } from '../accounting/subscription-model'; +import { getStartDate, getCancelledAt, getUpdatedAt } from "./chargebee-subscription-helper"; +import { SubscriptionModel } from "../accounting/subscription-model"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; +import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; +import { inject, injectable } from "inversify"; +import { + CostCenter_BillingStrategy, + UsageServiceClient, + UsageServiceDefinition, +} from "@gitpod/usage-api/lib/usage/v1/usage.pb"; /** * This class updates our internal Gitpod Subscription model with the events coming from the payment provider Chargebee @@ -36,42 +43,49 @@ import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; * * @see AccountingProtocol */ +@injectable() export class SubscriptionMapper { - - protected model: SubscriptionModel; + @inject(UsageServiceDefinition.name) + protected readonly usageService: UsageServiceClient; /** * @see SubscriptionMapper * @param gitpodSubscriptions * @param chargebeeEvent */ - map(gitpodSubscriptions: Subscription[], chargebeeEvent: chargebee.Event): SubscriptionModel { + map( + gitpodSubscriptions: Subscription[], + chargebeeEvent: chargebee.Event, + ): SubscriptionModel { const eventType = chargebeeEvent.event_type; const chargebeeSubscription = chargebeeEvent.content.subscription; - this.model = new SubscriptionModel(chargebeeSubscription.customer_id, gitpodSubscriptions.sort(orderByEndDateDescThenStartDateDesc)); + const model = new SubscriptionModel( + chargebeeSubscription.customer_id, + gitpodSubscriptions.sort(orderByEndDateDescThenStartDateDesc), + ); switch (eventType) { - case 'subscription_created': - this.handleSubscriptionCreated(chargebeeSubscription); + case "subscription_created": + this.handleSubscriptionCreated(chargebeeSubscription, model); break; - case 'subscription_changed': - this.handleSubscriptionChanged(chargebeeSubscription); + case "subscription_changed": + this.handleSubscriptionChanged(chargebeeSubscription, model); break; - case 'subscription_cancelled': - this.handleSubscriptionCancelled(chargebeeSubscription); + case "subscription_cancelled": + this.handleSubscriptionCancelled(chargebeeSubscription, model); break; - case 'subscription_reactivated': - this.handleSubscriptionReactivated(chargebeeSubscription); + case "subscription_reactivated": + this.handleSubscriptionReactivated(chargebeeSubscription, model); break; - case 'subscription_changes_scheduled': - this.handleSubscriptionChangesScheduled(chargebeeSubscription); + case "subscription_changes_scheduled": + this.handleSubscriptionChangesScheduled(chargebeeSubscription, model); break; - case 'subscription_scheduled_changes_removed': - this.handleSubscriptionScheduledChangesRemoved(chargebeeSubscription); + case "subscription_scheduled_changes_removed": + this.handleSubscriptionScheduledChangesRemoved(chargebeeSubscription, model); break; default: } - return this.model; + return model; } /** @@ -79,14 +93,16 @@ export class SubscriptionMapper { * * @param chargebeeSubscription */ - protected handleSubscriptionCreated(chargebeeSubscription: chargebee.Subscription) { - this.model.add(Subscription.create({ - userId: chargebeeSubscription.customer_id, - startDate: getStartDate(chargebeeSubscription), - planId: chargebeeSubscription.plan_id, - amount: Plans.getHoursPerMonth(getPlan(chargebeeSubscription)), - paymentReference: chargebeeSubscription.id - })); + protected handleSubscriptionCreated(chargebeeSubscription: chargebee.Subscription, model: SubscriptionModel) { + model.add( + Subscription.create({ + userId: chargebeeSubscription.customer_id, + startDate: getStartDate(chargebeeSubscription), + planId: chargebeeSubscription.plan_id, + amount: Plans.getHoursPerMonth(getPlan(chargebeeSubscription)), + paymentReference: chargebeeSubscription.id, + }), + ); } /** @@ -100,9 +116,9 @@ export class SubscriptionMapper { * @param gitpodSubscriptions * @param chargebeeSubscription */ - protected handleSubscriptionChanged(chargebeeSubscription: chargebee.Subscription) { + protected handleSubscriptionChanged(chargebeeSubscription: chargebee.Subscription, model: SubscriptionModel) { // Find old Gitpod subscription for the incoming, updated Chargebee Subscription - const oldSubscription = this.model.findSubscriptionByPaymentReference(chargebeeSubscription.id); + const oldSubscription = model.findSubscriptionByPaymentReference(chargebeeSubscription.id); const chargebeePlan = getPlan(chargebeeSubscription); const newAmount = Plans.getHoursPerMonth(chargebeePlan); const oldPlan = Plans.getById(oldSubscription.planId); @@ -124,22 +140,27 @@ export class SubscriptionMapper { log.warn("Downgrade without downgradeDate!"); dateInLastPeriod = new Date(); } - const { endDate: oldEndDate } = Subscription.calculateCurrentPeriod(oldSubscription.startDate, dateInLastPeriod); - this.model.cancel(oldSubscription, oldEndDate, oldEndDate); + const { endDate: oldEndDate } = Subscription.calculateCurrentPeriod( + oldSubscription.startDate, + dateInLastPeriod, + ); + model.cancel(oldSubscription, oldEndDate, oldEndDate); // 2. Create new Gitpod subscription with same paymentReference but new billing period - this.model.add(Subscription.create({ - userId: chargebeeSubscription.customer_id, - startDate: oldEndDate, - planId: chargebeeSubscription.plan_id, - amount: newAmount, - paymentReference: chargebeeSubscription.id - })); + model.add( + Subscription.create({ + userId: chargebeeSubscription.customer_id, + startDate: oldEndDate, + planId: chargebeeSubscription.plan_id, + amount: newAmount, + paymentReference: chargebeeSubscription.id, + }), + ); } else if (subscriptionChange === "upgrade") { // Upgrade oldSubscription.amount = newAmount; oldSubscription.planId = chargebeeSubscription.plan_id; - this.model.update(oldSubscription); + model.update(oldSubscription); } else { // As of now we're not interested in other updates } @@ -152,12 +173,37 @@ export class SubscriptionMapper { * * @param chargebeeSubscription */ - protected handleSubscriptionCancelled(chargebeeSubscription: chargebee.Subscription) { - const gitpodSubscription = this.model.findSubscriptionByPaymentReference(chargebeeSubscription.id); + protected handleSubscriptionCancelled(chargebeeSubscription: chargebee.Subscription, model: SubscriptionModel) { + const gitpodSubscription = model.findSubscriptionByPaymentReference(chargebeeSubscription.id); const cancellationDate = getCancelledAt(chargebeeSubscription); const endDate = calculateCurrentTermEnd(gitpodSubscription.startDate, cancellationDate); - this.model.cancel(gitpodSubscription, cancellationDate, endDate); + model.cancel(gitpodSubscription, cancellationDate, endDate); + + this.markChargbeeCancellationInNewUsageComponent(gitpodSubscription).catch((error) => { + log.error( + { userId: gitpodSubscription.userId }, + "Couldn't create chargebee cancellation costcenter.", + { + gitpodSubscription, + chargebeeSubscription, + }, + error, + ); + }); + } + + private async markChargbeeCancellationInNewUsageComponent(sub: Subscription) { + const { costCenter } = await this.usageService.getCostCenter({ + attributionId: AttributionId.render({ kind: "user", userId: sub.userId }), + }); + if (!costCenter) { + throw new Error(`No cost center found for user ${sub.userId}.`); + } + costCenter.billingStrategy = CostCenter_BillingStrategy.BILLING_STRATEGY_CHARGEBEE_CANCELLATION; + costCenter.nextBillingTime = new Date(sub.endDate || sub.cancellationDate!); + costCenter.spendingLimit = sub.amount * 10; // workspace hours * 10 = credits + await this.usageService.setCostCenter({ costCenter }); } /** @@ -166,14 +212,16 @@ export class SubscriptionMapper { * * @param chargebeeSubscription */ - protected handleSubscriptionReactivated(chargebeeSubscription: chargebee.Subscription) { - this.model.add(Subscription.create({ - userId: chargebeeSubscription.customer_id, - startDate: getStartDate(chargebeeSubscription), - planId: chargebeeSubscription.plan_id, - amount: Plans.getHoursPerMonth(getPlan(chargebeeSubscription)), - paymentReference: chargebeeSubscription.id - })); + protected handleSubscriptionReactivated(chargebeeSubscription: chargebee.Subscription, model: SubscriptionModel) { + model.add( + Subscription.create({ + userId: chargebeeSubscription.customer_id, + startDate: getStartDate(chargebeeSubscription), + planId: chargebeeSubscription.plan_id, + amount: Plans.getHoursPerMonth(getPlan(chargebeeSubscription)), + paymentReference: chargebeeSubscription.id, + }), + ); } /** @@ -181,15 +229,21 @@ export class SubscriptionMapper { * * @param chargebeeSubscription */ - protected handleSubscriptionChangesScheduled(chargebeeSubscription: chargebee.Subscription) { - const gitpodSubscription = this.model.findSubscriptionByPaymentReference(chargebeeSubscription.id); - const { endDate: downgradeEffectiveDate } = Subscription.calculateCurrentPeriod(gitpodSubscription.startDate, new Date(getUpdatedAt(chargebeeSubscription))); + protected handleSubscriptionChangesScheduled( + chargebeeSubscription: chargebee.Subscription, + model: SubscriptionModel, + ) { + const gitpodSubscription = model.findSubscriptionByPaymentReference(chargebeeSubscription.id); + const { endDate: downgradeEffectiveDate } = Subscription.calculateCurrentPeriod( + gitpodSubscription.startDate, + new Date(getUpdatedAt(chargebeeSubscription)), + ); if (gitpodSubscription.paymentData) { gitpodSubscription.paymentData.downgradeDate = downgradeEffectiveDate; } else { gitpodSubscription.paymentData = { downgradeDate: downgradeEffectiveDate }; } - this.model.update(gitpodSubscription); + model.update(gitpodSubscription); } /** @@ -197,11 +251,14 @@ export class SubscriptionMapper { * * @param chargebeeSubscription */ - protected handleSubscriptionScheduledChangesRemoved(chargebeeSubscription: chargebee.Subscription) { - const gitpodSubscription = this.model.findSubscriptionByPaymentReference(chargebeeSubscription.id); + protected handleSubscriptionScheduledChangesRemoved( + chargebeeSubscription: chargebee.Subscription, + model: SubscriptionModel, + ) { + const gitpodSubscription = model.findSubscriptionByPaymentReference(chargebeeSubscription.id); if (gitpodSubscription.paymentData) { gitpodSubscription.paymentData.downgradeDate = undefined; - this.model.update(gitpodSubscription); + model.update(gitpodSubscription); } } } @@ -216,7 +273,9 @@ const calculateCurrentTermEnd = (startDate: string, upTo: string): string => { const termStartDate = new Date(startDate); const upToDate = new Date(upTo); if (termStartDate.getTime() >= upToDate.getTime()) { - throw new Error(`calculateCurrentTermEnd: termStart (${termStartDate}) must be less than potential term end (${upToDate.getTime()})`); + throw new Error( + `calculateCurrentTermEnd: termStart (${termStartDate}) must be less than potential term end (${upToDate.getTime()})`, + ); } const dayOfMonth = termStartDate.getDate(); let potentialCurrentTermEnd = termStartDate; @@ -233,8 +292,3 @@ const getPlan = (chargebeeSubscription: chargebee.Subscription) => { } return plan; }; - -export const SubscriptionMapperFactory = Symbol('SubscriptionMapperFactory'); -export interface SubscriptionMapperFactory { - newMapper(): SubscriptionMapper; -} \ No newline at end of file diff --git a/components/ee/payment-endpoint/src/config.ts b/components/ee/payment-endpoint/src/config.ts index 8add7f885714a6..a8a2cbc327253f 100644 --- a/components/ee/payment-endpoint/src/config.ts +++ b/components/ee/payment-endpoint/src/config.ts @@ -5,22 +5,23 @@ */ import { injectable } from "inversify"; -import * as fs from 'fs'; +import * as fs from "fs"; -import { AbstractComponentEnv, getEnvVar, filePathTelepresenceAware } from '@gitpod/gitpod-protocol/lib/env'; +import { AbstractComponentEnv, getEnvVar, filePathTelepresenceAware } from "@gitpod/gitpod-protocol/lib/env"; import { parseOptions } from "./chargebee/chargebee-provider"; @injectable() export class Config extends AbstractComponentEnv { - readonly chargebeeProviderOptions = parseOptions(fs.readFileSync(filePathTelepresenceAware('/chargebee/providerOptions')).toString()); + readonly chargebeeProviderOptions = parseOptions( + fs.readFileSync(filePathTelepresenceAware("/chargebee/providerOptions")).toString(), + ); readonly chargebeeWebhook = this.parseChargebeeWebhook(); protected parseChargebeeWebhook(): ChargebeeWebhook { - const envVar = getEnvVar('CHARGEBEE_WEBHOOK'); + const envVar = getEnvVar("CHARGEBEE_WEBHOOK"); const obj = JSON.parse(envVar) as ChargebeeWebhook; - if (!('user' in obj && - 'password' in obj)) { - throw new Error('Unable to parse chargebee webhook config from string: ' + envVar); + if (!("user" in obj && "password" in obj)) { + throw new Error("Unable to parse chargebee webhook config from string: " + envVar); } return obj; } @@ -36,11 +37,14 @@ export GITPOD_GITHUB_APP_MKT_NAME=gitpod-draft-development-app readonly githubAppAppID: number = process.env.GITPOD_GITHUB_APP_ID ? parseInt(process.env.GITPOD_GITHUB_APP_ID) : 0; readonly githubAppWebhookSecret: string = process.env.GITPOD_GITHUB_APP_WEBHOOK_SECRET || "unknown"; readonly githubAppCertPath: string = process.env.GITPOD_GITHUB_APP_CERT_PATH || "unknown"; + readonly usageServiceAddr: string = process.env.GITPOD_USAGE_SERVICE_ADDR || "usage.default.svc.cluster.local:9001"; - readonly maxTeamSlotsOnCreation: number = !!process.env.TS_MAX_SLOTS_ON_CREATION ? parseInt(process.env.TS_MAX_SLOTS_ON_CREATION) : 1000; + readonly maxTeamSlotsOnCreation: number = !!process.env.TS_MAX_SLOTS_ON_CREATION + ? parseInt(process.env.TS_MAX_SLOTS_ON_CREATION) + : 1000; } export interface ChargebeeWebhook { user: string; password: string; -} \ No newline at end of file +} diff --git a/components/ee/payment-endpoint/src/container-module.ts b/components/ee/payment-endpoint/src/container-module.ts index a9971f25b6975b..ffc0ecc354bef7 100644 --- a/components/ee/payment-endpoint/src/container-module.ts +++ b/components/ee/payment-endpoint/src/container-module.ts @@ -12,7 +12,7 @@ import { SubscriptionService } from "./accounting/subscription-service"; import { Config } from "./config"; import { Server } from "./server"; import { SubscriptionHandler } from "./chargebee/subscription-handler"; -import { SubscriptionMapperFactory, SubscriptionMapper } from "./chargebee/subscription-mapper"; +import { SubscriptionMapper } from "./chargebee/subscription-mapper"; import { TeamSubscriptionHandler } from "./chargebee/team-subscription-handler"; import { CompositeEventHandler, EventHandler } from "./chargebee/chargebee-event-handler"; import { UpgradeHelper } from "./chargebee/upgrade-helper"; @@ -23,22 +23,17 @@ import { AccountServiceImpl } from "./accounting/account-service-impl"; import { GithubEndpointController } from "./github/endpoint-controller"; import { GithubSubscriptionMapper } from "./github/subscription-mapper"; import { GithubSubscriptionReconciler } from "./github/subscription-reconciler"; - +import { UsageServiceClient, UsageServiceDefinition } from "@gitpod/usage-api/lib/usage/v1/usage.pb"; +import { createChannel, createClient } from "nice-grpc"; +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; export const productionContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(Config).toSelf().inSingletonScope(); bind(Server).toSelf().inSingletonScope(); bind(EndpointController).toSelf().inSingletonScope(); bind(SubscriptionHandler).toSelf().inSingletonScope(); - bind(SubscriptionMapperFactory).toDynamicValue(ctx => { - return { - newMapper: () => { - return new SubscriptionMapper(); - } - }; - }); + bind(SubscriptionMapper).toSelf().inSingletonScope(); bind(TeamSubscriptionHandler).toSelf().inSingletonScope(); bind(CompositeEventHandler).toSelf().inSingletonScope(); @@ -51,14 +46,23 @@ export const productionContainerModule = new ContainerModule((bind, unbind, isBo bind(AccountService).to(AccountServiceImpl).inSingletonScope(); bind(ChargebeeProvider).toSelf().inSingletonScope(); - bind(ChargebeeProviderOptions).toDynamicValue(ctx => { - const config = ctx.container.get(Config); - return config.chargebeeProviderOptions; - }).inSingletonScope(); + bind(ChargebeeProviderOptions) + .toDynamicValue((ctx) => { + const config = ctx.container.get(Config); + return config.chargebeeProviderOptions; + }) + .inSingletonScope(); bind(UpgradeHelper).toSelf().inSingletonScope(); bind(GithubEndpointController).toSelf().inSingletonScope(); bind(GithubSubscriptionMapper).toSelf().inSingletonScope(); bind(GithubSubscriptionReconciler).toSelf().inSingletonScope(); + bind(UsageServiceDefinition.name) + .toDynamicValue((ctx) => { + const config = ctx.container.get(Config); + log.info("Connecting to usage service at", { addr: config.usageServiceAddr }); + return createClient(UsageServiceDefinition, createChannel(config.usageServiceAddr)); + }) + .inSingletonScope(); }); diff --git a/components/gitpod-db/go/cost_center.go b/components/gitpod-db/go/cost_center.go index cfb50de79e7c0a..bf52dae791322c 100644 --- a/components/gitpod-db/go/cost_center.go +++ b/components/gitpod-db/go/cost_center.go @@ -132,7 +132,9 @@ func getCostCenter(ctx context.Context, conn *gorm.DB, attributionId Attribution return costCenter, nil } -func (c *CostCenterManager) UpdateCostCenter(ctx context.Context, newCC CostCenter) (CostCenter, error) { +func (c *CostCenterManager) UpdateCostCenter(ctx context.Context, costCenter CostCenter) (CostCenter, error) { + // create a copy + newCC := costCenter if newCC.SpendingLimit < 0 { return CostCenter{}, status.Errorf(codes.InvalidArgument, "Spending limit cannot be set below zero.") } @@ -148,9 +150,12 @@ func (c *CostCenterManager) UpdateCostCenter(ctx context.Context, newCC CostCent // we always update the creationTime newCC.CreationTime = NewVarCharTime(now) - // we don't allow setting billingCycleStart or nextBillingTime from outside + // we don't allow setting billingCycleStart from outside newCC.BillingCycleStart = existingCC.BillingCycleStart - newCC.NextBillingTime = existingCC.NextBillingTime + // setting nextBillingTime is only possible for chargebee cancellation + if newCC.BillingStrategy != CostCenter_ChargebeeCancelled { + newCC.NextBillingTime = existingCC.NextBillingTime + } isTeam := attributionID.IsEntity(AttributionEntity_Team) isUser := attributionID.IsEntity(AttributionEntity_User) diff --git a/components/usage/pkg/apiv1/usage.go b/components/usage/pkg/apiv1/usage.go index a05c5da693650a..6ded07997addf0 100644 --- a/components/usage/pkg/apiv1/usage.go +++ b/components/usage/pkg/apiv1/usage.go @@ -234,6 +234,7 @@ func (s *UsageService) SetCostCenter(ctx context.Context, in *v1.SetCostCenterRe ID: attrID, SpendingLimit: in.CostCenter.SpendingLimit, BillingStrategy: convertBillingStrategyToDB(in.CostCenter.BillingStrategy), + NextBillingTime: db.NewVarCharTime(in.CostCenter.NextBillingTime.AsTime()), } result, err := s.costCenterManager.UpdateCostCenter(ctx, costCenter) if err != nil { diff --git a/components/usage/pkg/apiv1/usage_test.go b/components/usage/pkg/apiv1/usage_test.go index 8eedf0aa3a9ad6..7c570d882b16c1 100644 --- a/components/usage/pkg/apiv1/usage_test.go +++ b/components/usage/pkg/apiv1/usage_test.go @@ -245,8 +245,15 @@ func TestGetAndSetCostCenter(t *testing.T) { { AttributionId: string(db.NewTeamAttributionID(uuid.New().String())), SpendingLimit: 0, + NextBillingTime: timestamppb.New(time.Now().Add(12 * time.Hour)), BillingStrategy: v1.CostCenter_BILLING_STRATEGY_OTHER, }, + { + AttributionId: string(db.NewTeamAttributionID(uuid.New().String())), + SpendingLimit: 3000, + NextBillingTime: timestamppb.New(time.Now().Add(12 * time.Hour)), + BillingStrategy: v1.CostCenter_BILLING_STRATEGY_CHARGEBEE_CANCELLATION, + }, } service := newUsageService(t, conn) @@ -259,6 +266,11 @@ func TestGetAndSetCostCenter(t *testing.T) { require.Equal(t, costCenter.SpendingLimit, retrieved.CostCenter.SpendingLimit) require.Equal(t, costCenter.BillingStrategy, retrieved.CostCenter.BillingStrategy) + if costCenter.BillingStrategy == v1.CostCenter_BILLING_STRATEGY_CHARGEBEE_CANCELLATION { + require.Equal(t, costCenter.NextBillingTime.String(), retrieved.CostCenter.NextBillingTime.String()) + } else { + require.NotEqual(t, costCenter.NextBillingTime, retrieved.CostCenter.NextBillingTime) + } } } diff --git a/install/installer/pkg/common/constants.go b/install/installer/pkg/common/constants.go index ac56fbf4f8ff2a..c4bb0ea69a30cd 100644 --- a/install/installer/pkg/common/constants.go +++ b/install/installer/pkg/common/constants.go @@ -40,6 +40,7 @@ const ( ServerComponent = "server" ServerInstallationAdminPort = 9000 SystemNodeCritical = "system-node-critical" + PaymentEndpointComponent = "payment-endpoint" PublicApiComponent = "public-api-server" WSManagerComponent = "ws-manager" WSManagerBridgeComponent = "ws-manager-bridge" diff --git a/install/installer/pkg/components/usage/networkpolicy.go b/install/installer/pkg/components/usage/networkpolicy.go index 850e7c531f82f3..e188800f423166 100644 --- a/install/installer/pkg/components/usage/networkpolicy.go +++ b/install/installer/pkg/components/usage/networkpolicy.go @@ -49,6 +49,13 @@ func networkpolicy(ctx *common.RenderContext) ([]runtime.Object, error) { }, }, }, + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "component": common.PaymentEndpointComponent, + }, + }, + }, }, }, },