From 601db2fd2db28382672632383d5934e5ae9514a7 Mon Sep 17 00:00:00 2001 From: Reino Muhl <10620585+StaberindeZA@users.noreply.github.com> Date: Fri, 24 Oct 2025 17:15:31 -0400 Subject: [PATCH] feat(payments-api): add Stripe webhook handler Because: - The payments-api service needs to be able to processes Stripe webhooks This commit: - Adds Controller, StripeWebhookService and related modules to payments-api Closes #PAY-3292 --- apps/payments/api/.env | 61 ++++- apps/payments/api/src/app/app.module.ts | 67 ++++- apps/payments/api/src/app/app.service.ts | 10 +- apps/payments/api/src/config/index.ts | 62 ++++- apps/payments/api/src/main.ts | 4 +- apps/payments/next/.env | 3 + .../metrics/src/lib/glean/glean.config.ts | 2 + .../paypal/src/lib/paypal.client.config.ts | 4 + .../stripe/src/lib/factories/event.factory.ts | 20 +- libs/payments/ui/src/lib/nestapp/config.ts | 16 +- libs/payments/webhooks/src/index.ts | 8 +- libs/payments/webhooks/src/lib/factories.ts | 36 ++- .../webhooks/src/lib/stripe-event.config.ts | 21 ++ .../src/lib/stripe-event.manager.spec.ts | 230 ++++++++++++++++++ .../webhooks/src/lib/stripe-event.manager.ts | 91 +++++++ ....ts => stripe-webhooks.controller.spec.ts} | 67 +++-- .../src/lib/stripe-webhooks.controller.ts | 29 +++ ...pec.ts => stripe-webhooks.service.spec.ts} | 25 +- ....service.ts => stripe-webhooks.service.ts} | 17 +- .../webhooks/src/lib/stripeEvents.manager.ts | 38 --- ...s => subscription-handler.service.spec.ts} | 20 +- ...ice.ts => subscription-handler.service.ts} | 12 +- .../cms/src/lib/strapi.client.config.ts | 4 + .../firestore/src/lib/firestore.provider.ts | 4 +- libs/shared/metrics/statsd/src/index.ts | 1 + .../metrics/statsd/src/lib/statsd.config.ts | 4 + 26 files changed, 704 insertions(+), 152 deletions(-) create mode 100644 libs/payments/webhooks/src/lib/stripe-event.config.ts create mode 100644 libs/payments/webhooks/src/lib/stripe-event.manager.spec.ts create mode 100644 libs/payments/webhooks/src/lib/stripe-event.manager.ts rename libs/payments/webhooks/src/lib/{stripeEvents.manager.spec.ts => stripe-webhooks.controller.spec.ts} (59%) create mode 100644 libs/payments/webhooks/src/lib/stripe-webhooks.controller.ts rename libs/payments/webhooks/src/lib/{stripeWebhooks.service.spec.ts => stripe-webhooks.service.spec.ts} (79%) rename libs/payments/webhooks/src/lib/{stripeWebhooks.service.ts => stripe-webhooks.service.ts} (75%) delete mode 100644 libs/payments/webhooks/src/lib/stripeEvents.manager.ts rename libs/payments/webhooks/src/lib/{subscriptionHandler.service.spec.ts => subscription-handler.service.spec.ts} (94%) rename libs/payments/webhooks/src/lib/{subscriptionHandler.service.ts => subscription-handler.service.ts} (92%) diff --git a/apps/payments/api/.env b/apps/payments/api/.env index 486142741ca..61c8789aefa 100644 --- a/apps/payments/api/.env +++ b/apps/payments/api/.env @@ -1,3 +1,58 @@ -TEST_OVERRIDE="default override value" -TEST_DEFAULT="default value" -TEST_NESTED_CONFIG__TEST_NESTED="nested value" +# MySQLConfig +MYSQL_CONFIG__DATABASE=fxa +MYSQL_CONFIG__HOST=::1 +MYSQL_CONFIG__PORT=3306 +MYSQL_CONFIG__USER=root +MYSQL_CONFIG__PASSWORD= +MYSQL_CONFIG__CONNECTION_LIMIT_MIN= +MYSQL_CONFIG__CONNECTION_LIMIT_MAX=20 +MYSQL_CONFIG__ACQUIRE_TIMEOUT_MILLIS= + +# Stripe Config +STRIPE_CONFIG__API_KEY=11233 +STRIPE_CONFIG__WEBHOOK_SECRET=11233 +STRIPE_CONFIG__TAX_IDS={} + +# PayPal Config +PAYPAL_CLIENT_CONFIG__SANDBOX=true +PAYPAL_CLIENT_CONFIG__USER=ASDF +PAYPAL_CLIENT_CONFIG__PWD=ASDF +PAYPAL_CLIENT_CONFIG__SIGNATURE=ASDF +PAYPAL_CLIENT_CONFIG__RETRY_OPTIONS__RETRIES=1 +PAYPAL_CLIENT_CONFIG__RETRY_OPTIONS__MIN_TIMEOUT=1 +PAYPAL_CLIENT_CONFIG__RETRY_OPTIONS__FACTOR=1 + +# Strapi Config +STRAPI_CLIENT_CONFIG__GRAPHQL_API_URI=https://example.com +STRAPI_CLIENT_CONFIG__API_KEY=PLACEHOLDER +STRAPI_CLIENT_CONFIG__MEM_CACHE_T_T_L= +STRAPI_CLIENT_CONFIG__FIRESTORE_CACHE_COLLECTION_NAME=strapiClientQueryCacheCollection +STRAPI_CLIENT_CONFIG__FIRESTORE_CACHE_T_T_L= +STRAPI_CLIENT_CONFIG__FIRESTORE_OFFLINE_CACHE_T_T_L= + +# Firestore Config +FIRESTORE_CONFIG__CREDENTIALS__CLIENT_EMAIL= +FIRESTORE_CONFIG__CREDENTIALS__PRIVATE_KEY= +FIRESTORE_CONFIG__KEY_FILENAME= +FIRESTORE_CONFIG__PROJECT_ID= + +# Currency Config +CURRENCY_CONFIG__TAX_IDS={ "EUR": "EU1234", "CHF": "CH1234" } +CURRENCY_CONFIG__CURRENCIES_TO_COUNTRIES={ "USD": ["US", "GB", "NZ", "MY", "SG", "CA", "AS", "GU", "MP", "PR", "VI"], "EUR": ["FR", "DE"] } + +# StatsD Config +STATS_D_CONFIG__SAMPLE_RATE= +STATS_D_CONFIG__MAX_BUFFER_SIZE= +STATS_D_CONFIG__HOST= +STATS_D_CONFIG__PORT= +STATS_D_CONFIG__PREFIX= + +# Stripe Events Config +STRIPE_EVENTS_CONFIG__FIRESTORE_STRIPE_EVENT_STORE_COLLECTION_NAME=stripeEvents + +# Glean Config +GLEAN_CONFIG__ENABLED=false +GLEAN_CONFIG__APPLICATION_ID= +GLEAN_CONFIG__VERSION=0.0.0 +GLEAN_CONFIG__CHANNEL='development' +GLEAN_CONFIG__LOGGER_APP_NAME='fxa-payments-next' diff --git a/apps/payments/api/src/app/app.module.ts b/apps/payments/api/src/app/app.module.ts index 3419494b07f..540a9f88593 100644 --- a/apps/payments/api/src/app/app.module.ts +++ b/apps/payments/api/src/app/app.module.ts @@ -1,8 +1,41 @@ -import { Module } from '@nestjs/common'; +import { Logger, Module } from '@nestjs/common'; import { TypedConfigModule, dotenvLoader } from 'nest-typed-config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { RootConfig } from '../config'; +import { + StripeEventManager, + StripeWebhooksController, + StripeWebhookService, + SubscriptionEventsService, +} from '@fxa/payments/webhooks'; +import { FirestoreProvider } from '@fxa/shared/db/firestore'; +import { StripeClient } from '@fxa/payments/stripe'; +import { StatsDProvider } from '@fxa/shared/metrics/statsd'; +import { + CustomerManager, + InvoiceManager, + PaymentMethodManager, + PriceManager, + SubscriptionManager, +} from '@fxa/payments/customer'; +import { + PaypalBillingAgreementManager, + PayPalClient, + PaypalClientConfig, + PaypalCustomerManager, +} from '@fxa/payments/paypal'; +import { CurrencyManager } from '@fxa/payments/currency'; +import { AccountDatabaseNestFactory } from '@fxa/shared/db/mysql/account'; +import { AccountManager } from '@fxa/shared/account/account'; +import { CartManager } from '@fxa/payments/cart'; +import { ProductConfigurationManager, StrapiClient } from '@fxa/shared/cms'; +import { + MockPaymentsGleanFactory, + PaymentsGleanManager, +} from '@fxa/payments/metrics'; +import { PaymentsGleanFactory } from '@fxa/payments/metrics/provider'; +import { PaymentsEmitterService } from '@fxa/payments/events'; @Module({ imports: [ @@ -18,7 +51,35 @@ import { RootConfig } from '../config'; }), }), ], - controllers: [AppController], - providers: [AppService], + controllers: [AppController, StripeWebhooksController], + providers: [ + Logger, + AccountDatabaseNestFactory, + AccountManager, + AppService, + ProductConfigurationManager, + CartManager, + SubscriptionEventsService, + PaymentsGleanFactory, + PaymentsGleanManager, + PaymentsEmitterService, + PriceManager, + FirestoreProvider, + StatsDProvider, + StripeClient, + PayPalClient, + PaypalClientConfig, + SubscriptionManager, + CustomerManager, + InvoiceManager, + PaymentMethodManager, + CurrencyManager, + StripeWebhookService, + StripeEventManager, + PaypalBillingAgreementManager, + PaypalCustomerManager, + StrapiClient, + MockPaymentsGleanFactory, + ], }) export class AppModule {} diff --git a/apps/payments/api/src/app/app.service.ts b/apps/payments/api/src/app/app.service.ts index 4218b8f9fb3..0e95e8d7362 100644 --- a/apps/payments/api/src/app/app.service.ts +++ b/apps/payments/api/src/app/app.service.ts @@ -1,24 +1,18 @@ import { Injectable } from '@nestjs/common'; -import { RootConfig, TestNestedConfig } from '../config'; +import { RootConfig } from '../config'; @Injectable() export class AppService { - constructor( - private config: RootConfig, - private nestedConfig: TestNestedConfig - ) {} + constructor(private config: RootConfig) {} getData(): { message: string; config: RootConfig; - nestedConfigOnly: TestNestedConfig; } { console.log('All config', this.config); - console.log('Nested only', this.nestedConfig); return { message: 'Hello API', config: this.config, - nestedConfigOnly: this.nestedConfig, }; } } diff --git a/apps/payments/api/src/config/index.ts b/apps/payments/api/src/config/index.ts index 6bdbe1dc3ed..71ae7c517f2 100644 --- a/apps/payments/api/src/config/index.ts +++ b/apps/payments/api/src/config/index.ts @@ -1,19 +1,59 @@ import { Type } from 'class-transformer'; -import { IsString, ValidateNested } from 'class-validator'; +import { IsDefined, ValidateNested } from 'class-validator'; -export class TestNestedConfig { - @IsString() - public readonly testNested!: string; -} +import { CurrencyConfig } from '@fxa/payments/currency'; +import { PaymentsGleanConfig } from '@fxa/payments/metrics'; +import { PaypalClientConfig } from '@fxa/payments/paypal'; +import { StripeConfig } from '@fxa/payments/stripe'; +import { StrapiClientConfig } from '@fxa/shared/cms'; +import { MySQLConfig } from '@fxa/shared/db/mysql/core'; +import { StripeEventConfig } from '@fxa/payments/webhooks'; +import { StatsDConfig } from '@fxa/shared/metrics/statsd'; +import { FirestoreConfig } from 'libs/shared/db/firestore/src/lib/firestore.config'; export class RootConfig { - @IsString() - public readonly testOverride!: string; + @Type(() => MySQLConfig) + @ValidateNested() + @IsDefined() + public readonly mysqlConfig!: Partial; + + @Type(() => PaymentsGleanConfig) + @ValidateNested() + @IsDefined() + public readonly gleanConfig!: Partial; + + @Type(() => StripeConfig) + @ValidateNested() + @IsDefined() + public readonly stripeConfig!: Partial; + + @Type(() => PaypalClientConfig) + @ValidateNested() + @IsDefined() + public readonly paypalClientConfig!: Partial; - @IsString() - public readonly testDefault!: string; + @Type(() => CurrencyConfig) + @ValidateNested() + @IsDefined() + public readonly currencyConfig!: Partial; + + @Type(() => FirestoreConfig) + @ValidateNested() + @IsDefined() + public readonly firestoreConfig!: Partial; + + @Type(() => StatsDConfig) + @ValidateNested() + @IsDefined() + public readonly statsDConfig!: Partial; + + @Type(() => StrapiClientConfig) + @ValidateNested() + @IsDefined() + public readonly strapiClientConfig!: Partial; - @Type(() => TestNestedConfig) + @Type(() => StripeEventConfig) @ValidateNested() - public readonly testNestedConfig!: TestNestedConfig; + @IsDefined() + public readonly stripeEventsConfig!: Partial; } diff --git a/apps/payments/api/src/main.ts b/apps/payments/api/src/main.ts index ee84cede3f4..2deed5f09a7 100644 --- a/apps/payments/api/src/main.ts +++ b/apps/payments/api/src/main.ts @@ -8,7 +8,9 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app/app.module'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { + rawBody: true, + }); const globalPrefix = 'api'; app.setGlobalPrefix(globalPrefix); const port = process.env.PORT || 3000; diff --git a/apps/payments/next/.env b/apps/payments/next/.env index d76246e63fe..e3f5ac31fd0 100644 --- a/apps/payments/next/.env +++ b/apps/payments/next/.env @@ -139,6 +139,9 @@ CONTENT_SERVER_CLIENT_CONFIG__URL=http://localhost:3030 # GoogleClient Config GOOGLE_CLIENT_CONFIG__GOOGLE_MAPS_API_KEY= +# Stripe Events Config +STRIPE_EVENTS_CONFIG__FIRESTORE_STRIPE_EVENT_STORE_COLLECTION_NAME=stripeEvents + # Feature flags FEATURE_FLAG_SUB_MANAGE=true diff --git a/libs/payments/metrics/src/lib/glean/glean.config.ts b/libs/payments/metrics/src/lib/glean/glean.config.ts index 0ea9fd99498..a4eb0ef206b 100644 --- a/libs/payments/metrics/src/lib/glean/glean.config.ts +++ b/libs/payments/metrics/src/lib/glean/glean.config.ts @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { faker } from '@faker-js/faker'; import { Provider } from '@nestjs/common'; +import { Type } from 'class-transformer'; import { IsBoolean, IsEnum, IsString } from 'class-validator'; enum GleanChannel { @@ -12,6 +13,7 @@ enum GleanChannel { } export class PaymentsGleanConfig { + @Type(() => Boolean) @IsBoolean() enabled!: boolean; diff --git a/libs/payments/paypal/src/lib/paypal.client.config.ts b/libs/payments/paypal/src/lib/paypal.client.config.ts index 9e7ceaa9061..cb475d71fbf 100644 --- a/libs/payments/paypal/src/lib/paypal.client.config.ts +++ b/libs/payments/paypal/src/lib/paypal.client.config.ts @@ -6,17 +6,21 @@ import { Type } from 'class-transformer'; import { IsBoolean, IsNumber, IsString, ValidateNested } from 'class-validator'; export class PaypalRetryConfig { + @Type(() => Number) @IsNumber() public readonly retries!: number; + @Type(() => Number) @IsNumber() public readonly minTimeout!: number; + @Type(() => Number) @IsNumber() public readonly factor!: number; } export class PaypalClientConfig { + @Type(() => Boolean) @IsBoolean() public readonly sandbox!: boolean; diff --git a/libs/payments/stripe/src/lib/factories/event.factory.ts b/libs/payments/stripe/src/lib/factories/event.factory.ts index 9a679e25073..af8e8b2bda6 100644 --- a/libs/payments/stripe/src/lib/factories/event.factory.ts +++ b/libs/payments/stripe/src/lib/factories/event.factory.ts @@ -9,39 +9,43 @@ import { StripeSubscription } from '../stripe.client.types'; // TODO - Create generic factory export const StripeEventCustomerSubscriptionCreatedFactory = ( + override?: Partial, dataObjectOverride?: Partial ): Stripe.Event => ({ id: 'evt_123', object: 'event', api_version: '2019-02-19', created: faker.date.past().getTime(), + livemode: false, + request: null, + pending_webhooks: 0, + ...override, + type: 'customer.subscription.created', data: { object: { ...StripeSubscriptionFactory(), ...dataObjectOverride, }, }, - livemode: false, - request: null, - pending_webhooks: 0, - type: 'customer.subscription.created', }); export const StripeEventCustomerSubscriptionDeletedFactory = ( + override?: Partial, dataObjectOverride?: Partial ): Stripe.Event => ({ id: 'evt_123', object: 'event', api_version: '2019-02-19', created: faker.date.past().getTime(), + livemode: false, + request: null, + pending_webhooks: 0, + ...override, + type: 'customer.subscription.deleted', data: { object: { ...StripeSubscriptionFactory(), ...dataObjectOverride, }, }, - livemode: false, - request: null, - pending_webhooks: 0, - type: 'customer.subscription.deleted', }); diff --git a/libs/payments/ui/src/lib/nestapp/config.ts b/libs/payments/ui/src/lib/nestapp/config.ts index 836f6232c96..3686952ff9b 100644 --- a/libs/payments/ui/src/lib/nestapp/config.ts +++ b/libs/payments/ui/src/lib/nestapp/config.ts @@ -9,18 +9,19 @@ import { GoogleClientConfig } from '@fxa/google'; import { MySQLConfig } from '@fxa/shared/db/mysql/core'; import { GeoDBConfig, GeoDBManagerConfig } from '@fxa/shared/geodb'; import { LocationConfig } from '@fxa/payments/eligibility'; -import { PaypalClientConfig } from 'libs/payments/paypal/src/lib/paypal.client.config'; +import { PaypalClientConfig } from '@fxa/payments/paypal'; import { StripeConfig } from '@fxa/payments/stripe'; import { StrapiClientConfig } from '@fxa/shared/cms'; -import { FirestoreConfig } from 'libs/shared/db/firestore/src/lib/firestore.config'; -import { StatsDConfig } from 'libs/shared/metrics/statsd/src/lib/statsd.config'; +import { StatsDConfig } from '@fxa/shared/metrics/statsd'; import { PaymentsGleanConfig } from '@fxa/payments/metrics'; -import { CurrencyConfig } from 'libs/payments/currency/src/lib/currency.config'; +import { CurrencyConfig } from '@fxa/payments/currency'; import { ProfileClientConfig } from '@fxa/profile/client'; import { ContentServerClientConfig } from '@fxa/payments/content-server'; import { NotifierSnsConfig } from '@fxa/shared/notifier'; import { AppleIapClientConfig, GoogleIapClientConfig } from '@fxa/payments/iap'; import { TracingConfig } from './tracing.config'; +import { StripeEventConfig } from '@fxa/payments/webhooks'; +import { FirestoreConfig } from 'libs/shared/db/firestore/src/lib/firestore.config'; export class RootConfig { @Type(() => MySQLConfig) @@ -41,7 +42,7 @@ export class RootConfig { @ValidateNested() @IsDefined() public readonly stripeConfig!: Partial; - + @Type(() => TracingConfig) @ValidateNested() @IsDefined() @@ -107,6 +108,11 @@ export class RootConfig { @IsDefined() public readonly googleClientConfig!: Partial; + @Type(() => StripeEventConfig) + @ValidateNested() + @IsDefined() + public readonly stripeEventsConfig!: Partial; + @Type(() => LocationConfig) @ValidateNested() @IsDefined() diff --git a/libs/payments/webhooks/src/index.ts b/libs/payments/webhooks/src/index.ts index 7d66cb4d92f..09e342de120 100644 --- a/libs/payments/webhooks/src/index.ts +++ b/libs/payments/webhooks/src/index.ts @@ -2,8 +2,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +export * from './lib/stripe-event.config'; export * from './lib/stripe-event-store.repository'; -export * from './lib/stripeWebhooks.service'; -export * from './lib/stripeEvents.manager'; -export * from './lib/subscriptionHandler.service'; +export * from './lib/stripe-webhooks.controller'; +export * from './lib/stripe-webhooks.service'; +export * from './lib/stripe-event.manager'; +export * from './lib/subscription-handler.service'; export * from './lib/types'; diff --git a/libs/payments/webhooks/src/lib/factories.ts b/libs/payments/webhooks/src/lib/factories.ts index e53bec534e6..4cd58ded059 100644 --- a/libs/payments/webhooks/src/lib/factories.ts +++ b/libs/payments/webhooks/src/lib/factories.ts @@ -20,25 +20,35 @@ export const CustomerSubscriptionDeletedResponseFactory = ( override?: Partial ): CustomerSubscriptionDeletedResponse => ({ type: 'customer.subscription.deleted', - event: StripeEventCustomerSubscriptionCreatedFactory(subscription), + event: StripeEventCustomerSubscriptionCreatedFactory(undefined, subscription), eventObjectData: subscription, ...override, }); export const StripeEventStoreEntryFactory = ( override?: Partial -): StripeEventStoreEntry => ({ - eventId: `evt_${faker.string.alphanumeric({ length: 24 })}`, - processedAt: new Date(), - eventDetails: StripeEventCustomerSubscriptionCreatedFactory(), - ...override, -}); +): StripeEventStoreEntry => { + const eventId = `evt_${faker.string.alphanumeric({ length: 24 })}`; + return { + eventId, + processedAt: new Date(), + eventDetails: StripeEventCustomerSubscriptionCreatedFactory({ + id: eventId, + }), + ...override, + }; +}; export const StripeEventStoreEntryFirestoreRecordFactory = ( override?: Partial -): StripeEventStoreEntryFirestoreRecord => ({ - eventId: `evt_${faker.string.alphanumeric({ length: 24 })}`, - processedAt: Timestamp.fromDate(new Date()), - eventDetails: StripeEventCustomerSubscriptionCreatedFactory(), - ...override, -}); +): StripeEventStoreEntryFirestoreRecord => { + const eventId = `evt_${faker.string.alphanumeric({ length: 24 })}`; + return { + eventId, + processedAt: Timestamp.fromDate(new Date()), + eventDetails: StripeEventCustomerSubscriptionCreatedFactory({ + id: eventId, + }), + ...override, + }; +}; diff --git a/libs/payments/webhooks/src/lib/stripe-event.config.ts b/libs/payments/webhooks/src/lib/stripe-event.config.ts new file mode 100644 index 00000000000..f51f6f224a7 --- /dev/null +++ b/libs/payments/webhooks/src/lib/stripe-event.config.ts @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { faker } from '@faker-js/faker'; +import { Provider } from '@nestjs/common'; +import { IsString } from 'class-validator'; + +export class StripeEventConfig { + @IsString() + public readonly firestoreStripeEventStoreCollectionName!: string; +} + +export const MockStripeEventConfig = { + firestoreStripeEventStoreCollectionName: faker.string.uuid(), +} satisfies StripeEventConfig; + +export const MockStripeEventConfigProvider = { + provide: StripeEventConfig, + useValue: MockStripeEventConfig, +} satisfies Provider; diff --git a/libs/payments/webhooks/src/lib/stripe-event.manager.spec.ts b/libs/payments/webhooks/src/lib/stripe-event.manager.spec.ts new file mode 100644 index 00000000000..6413088a94c --- /dev/null +++ b/libs/payments/webhooks/src/lib/stripe-event.manager.spec.ts @@ -0,0 +1,230 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Test } from '@nestjs/testing'; + +import { + MockStripeConfigProvider, + StripeClient, + StripeEventCustomerSubscriptionCreatedFactory, +} from '@fxa/payments/stripe'; +import { StripeEventManager } from './stripe-event.manager'; +import { StripeWebhookService } from './stripe-webhooks.service'; +import { SubscriptionEventsService } from './subscription-handler.service'; +import { + CustomerManager, + InvoiceManager, + PriceManager, + SubscriptionManager, + PaymentMethodManager, +} from '@fxa/payments/customer'; +import { PaymentsEmitterService } from '@fxa/payments/events'; +import { + MockPaypalClientConfigProvider, + PayPalClient, +} from '@fxa/payments/paypal'; +import { + CurrencyManager, + MockCurrencyConfigProvider, +} from '@fxa/payments/currency'; +import { + MockPaymentsGleanConfigProvider, + MockPaymentsGleanFactory, + PaymentsGleanManager, +} from '@fxa/payments/metrics'; +import { + MockStrapiClientConfigProvider, + ProductConfigurationManager, + StrapiClient, +} from '@fxa/shared/cms'; +import { CartManager } from '@fxa/payments/cart'; +import { AccountManager } from '@fxa/shared/account/account'; +import { MockFirestoreProvider } from '@fxa/shared/db/firestore'; +import { MockStatsDProvider } from '@fxa/shared/metrics/statsd'; +import { MockAccountDatabaseNestFactory } from '@fxa/shared/db/mysql/account'; +import { StripeEventCustomerSubscriptionDeletedFactory } from 'libs/payments/stripe/src/lib/factories/event.factory'; +import { Logger } from '@nestjs/common'; + +import { + createStripeEventStoreEntry, + getStripeEventStoreEntry, +} from './stripe-event-store.repository'; +import { MockStripeEventConfigProvider } from './stripe-event.config'; +import { StripeEventStoreEntryFactory } from './factories'; +import { + StripeEventStoreEntryAlreadyExistsError, + StripeEventStoreEntryNotFoundError, +} from './stripe-event-store.error'; + +jest.mock('./stripe-event-store.repository'); +const mockedGetStripeEventStoreEntry = jest.mocked(getStripeEventStoreEntry); +const mockedCreateStripeEventStoreEntry = jest.mocked( + createStripeEventStoreEntry +); + +describe('StripeEventManager', () => { + let stripeEventManager: StripeEventManager; + let stripeClient: StripeClient; + + const mockLogger = { + error: jest.fn(), + warn: jest.fn(), + log: jest.fn(), + }; + + const paymentMethodManagerMock = { + determineType: jest.fn(), + }; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + { + provide: Logger, + useValue: mockLogger, + }, + { + provide: PaymentMethodManager, + useValue: paymentMethodManagerMock, + }, + MockStripeConfigProvider, + MockStripeEventConfigProvider, + StripeClient, + StripeEventManager, + StripeWebhookService, + SubscriptionEventsService, + SubscriptionManager, + CustomerManager, + InvoiceManager, + PaymentsEmitterService, + PayPalClient, + CurrencyManager, + MockCurrencyConfigProvider, + PaymentsGleanManager, + MockPaymentsGleanConfigProvider, + MockPaymentsGleanFactory, + ProductConfigurationManager, + CartManager, + AccountManager, + MockPaypalClientConfigProvider, + MockStrapiClientConfigProvider, + StrapiClient, + MockStrapiClientConfigProvider, + MockFirestoreProvider, + MockStatsDProvider, + PriceManager, + MockAccountDatabaseNestFactory, + ], + }).compile(); + + stripeEventManager = module.get(StripeEventManager); + stripeClient = module.get(StripeClient); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('SubscriptionEventsService', () => { + describe('constructWebhookEventResponse', () => { + it('customer.subscription.deleted - returns subscription object', () => { + jest + .spyOn(stripeClient, 'constructWebhookEvent') + .mockReturnValue(StripeEventCustomerSubscriptionDeletedFactory()); + const result = stripeEventManager.constructWebhookEventResponse({}, ''); + expect(result.type).toBe('customer.subscription.deleted'); + }); + + it('customer.subscription.created - returns default', () => { + jest + .spyOn(stripeClient, 'constructWebhookEvent') + .mockReturnValue(StripeEventCustomerSubscriptionCreatedFactory()); + const result = stripeEventManager.constructWebhookEventResponse({}, ''); + expect(result.type).toBe('customer.subscription.created'); + }); + }); + + describe('isProcessed', () => { + const mockEventStoreEntry = StripeEventStoreEntryFactory(); + beforeEach(() => { + mockedGetStripeEventStoreEntry.mockResolvedValue(mockEventStoreEntry); + }); + + it('returns true if event is processed', async () => { + const result = await stripeEventManager.isProcessed( + mockEventStoreEntry.eventId + ); + expect(result).toBeTruthy(); + expect(mockedGetStripeEventStoreEntry).toHaveBeenCalledWith( + undefined, + mockEventStoreEntry.eventId + ); + }); + + it('returns false if event is not processed', async () => { + mockedGetStripeEventStoreEntry.mockRejectedValue( + new StripeEventStoreEntryNotFoundError(mockEventStoreEntry.eventId) + ); + const result = await stripeEventManager.isProcessed( + mockEventStoreEntry.eventId + ); + expect(result).toBeFalsy(); + expect(mockedGetStripeEventStoreEntry).toHaveBeenCalledWith( + undefined, + mockEventStoreEntry.eventId + ); + }); + + it('throws an error if unexpected error occurs', async () => { + const expectedError = new Error('Unexpected error'); + mockedGetStripeEventStoreEntry.mockRejectedValue(expectedError); + await expect( + stripeEventManager.isProcessed(mockEventStoreEntry.eventId) + ).rejects.toThrow(expectedError); + }); + }); + + describe('markAsProcessed', () => { + const mockEventStoreEntry = StripeEventStoreEntryFactory(); + beforeEach(() => { + mockedCreateStripeEventStoreEntry.mockResolvedValue( + mockEventStoreEntry + ); + }); + + it('successfully creates a new record', async () => { + await stripeEventManager.markAsProcessed( + mockEventStoreEntry.eventDetails + ); + expect(mockedCreateStripeEventStoreEntry).toHaveBeenCalledWith( + undefined, + { + eventId: mockEventStoreEntry.eventId, + processedAt: expect.any(Date), + eventDetails: mockEventStoreEntry.eventDetails, + } + ); + }); + + it('logs a warning if entry already exists', async () => { + const expectedError = new StripeEventStoreEntryAlreadyExistsError( + mockEventStoreEntry.eventId + ); + mockedCreateStripeEventStoreEntry.mockRejectedValue(expectedError); + await stripeEventManager.markAsProcessed( + mockEventStoreEntry.eventDetails + ); + expect(mockLogger.warn).toHaveBeenCalledWith(expectedError); + }); + + it('throws an error if unexpected error occurs', async () => { + const expectedError = new Error('Unexpected error'); + mockedCreateStripeEventStoreEntry.mockRejectedValue(expectedError); + await expect( + stripeEventManager.markAsProcessed(mockEventStoreEntry.eventDetails) + ).rejects.toThrow(expectedError); + }); + }); + }); +}); diff --git a/libs/payments/webhooks/src/lib/stripe-event.manager.ts b/libs/payments/webhooks/src/lib/stripe-event.manager.ts new file mode 100644 index 00000000000..9992536ce6c --- /dev/null +++ b/libs/payments/webhooks/src/lib/stripe-event.manager.ts @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { StripeClient } from '@fxa/payments/stripe'; +import { Inject, Injectable, Logger } from '@nestjs/common'; +import Stripe from 'stripe'; +import { StripeWebhookEventResponse } from './types'; +import { FirestoreService } from '@fxa/shared/db/firestore'; +import type { CollectionReference, Firestore } from '@google-cloud/firestore'; +import { StripeEventConfig } from './stripe-event.config'; +import { + createStripeEventStoreEntry, + getStripeEventStoreEntry, +} from './stripe-event-store.repository'; +import { + StripeEventStoreEntryAlreadyExistsError, + StripeEventStoreEntryNotFoundError, +} from './stripe-event-store.error'; + +@Injectable() +export class StripeEventManager { + private stripeEventStoreCollection: CollectionReference; + constructor( + private stripeClient: StripeClient, + private config: StripeEventConfig, + @Inject(FirestoreService) private firestore: Firestore, + private logger: Logger + ) { + this.stripeEventStoreCollection = this.firestore.collection( + this.config.firestoreStripeEventStoreCollectionName + ); + } + + constructWebhookEventResponse( + payload: any, + signature: string + ): StripeWebhookEventResponse { + const stripeEvent = this.stripeClient.constructWebhookEvent( + payload, + signature + ); + + switch (stripeEvent.type) { + case 'customer.subscription.deleted': + return { + type: 'customer.subscription.deleted', + event: stripeEvent, + eventObjectData: stripeEvent.data.object as Stripe.Subscription, + }; + default: + return { + type: stripeEvent.type, + event: stripeEvent, + eventObjectData: stripeEvent.data.object, + }; + } + } + + async isProcessed(eventId: string) { + try { + const entry = await getStripeEventStoreEntry( + this.stripeEventStoreCollection, + eventId + ); + return !!entry.processedAt; + } catch (error) { + if (error instanceof StripeEventStoreEntryNotFoundError) { + return false; + } else { + throw error; + } + } + } + + async markAsProcessed(event: Stripe.Event) { + try { + await createStripeEventStoreEntry(this.stripeEventStoreCollection, { + eventId: event.id, + processedAt: new Date(), + eventDetails: event, + }); + } catch (error) { + if (error instanceof StripeEventStoreEntryAlreadyExistsError) { + this.logger.warn(error); + } else { + throw error; + } + } + } +} diff --git a/libs/payments/webhooks/src/lib/stripeEvents.manager.spec.ts b/libs/payments/webhooks/src/lib/stripe-webhooks.controller.spec.ts similarity index 59% rename from libs/payments/webhooks/src/lib/stripeEvents.manager.spec.ts rename to libs/payments/webhooks/src/lib/stripe-webhooks.controller.spec.ts index 92b4763dbeb..7ee64962c16 100644 --- a/libs/payments/webhooks/src/lib/stripeEvents.manager.spec.ts +++ b/libs/payments/webhooks/src/lib/stripe-webhooks.controller.spec.ts @@ -3,22 +3,19 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { Test } from '@nestjs/testing'; - -import { - MockStripeConfigProvider, - StripeClient, - StripeEventCustomerSubscriptionCreatedFactory, -} from '@fxa/payments/stripe'; -import { StripeEventManager } from './stripeEvents.manager'; -import { StripeWebhookService } from './stripeWebhooks.service'; -import { SubscriptionEventsService } from './subscriptionHandler.service'; +import { StripeWebhooksController } from './stripe-webhooks.controller'; +import { StripeWebhookService } from './stripe-webhooks.service'; +import { StripeEventManager } from './stripe-event.manager'; +import { SubscriptionEventsService } from './subscription-handler.service'; import { CustomerManager, InvoiceManager, + PaymentMethodManager, PriceManager, SubscriptionManager, - PaymentMethodManager, } from '@fxa/payments/customer'; +import { MockStripeConfigProvider, StripeClient } from '@fxa/payments/stripe'; +import { MockStripeEventConfigProvider } from './stripe-event.config'; import { PaymentsEmitterService } from '@fxa/payments/events'; import { MockPaypalClientConfigProvider, @@ -43,12 +40,11 @@ import { AccountManager } from '@fxa/shared/account/account'; import { MockFirestoreProvider } from '@fxa/shared/db/firestore'; import { MockStatsDProvider } from '@fxa/shared/metrics/statsd'; import { MockAccountDatabaseNestFactory } from '@fxa/shared/db/mysql/account'; -import { StripeEventCustomerSubscriptionDeletedFactory } from 'libs/payments/stripe/src/lib/factories/event.factory'; import { Logger } from '@nestjs/common'; -describe('StripeEventManager', () => { - let stripeEventManager: StripeEventManager; - let stripeClient: StripeClient; +describe('StripeWebhooksController', () => { + let stripeWebhooksController: StripeWebhooksController; + let stripeWebhookService: StripeWebhookService; const mockLogger = { error: jest.fn(), @@ -68,9 +64,11 @@ describe('StripeEventManager', () => { }, { provide: PaymentMethodManager, - useValue: paymentMethodManagerMock + useValue: paymentMethodManagerMock, }, + StripeWebhooksController, MockStripeConfigProvider, + MockStripeEventConfigProvider, StripeClient, StripeEventManager, StripeWebhookService, @@ -88,42 +86,33 @@ describe('StripeEventManager', () => { ProductConfigurationManager, CartManager, AccountManager, - MockPaypalClientConfigProvider, - MockStrapiClientConfigProvider, StrapiClient, + MockPaypalClientConfigProvider, MockStrapiClientConfigProvider, MockFirestoreProvider, MockStatsDProvider, PriceManager, MockAccountDatabaseNestFactory, + StripeWebhookService, + StripeEventManager, + SubscriptionEventsService, ], }).compile(); - stripeEventManager = module.get(StripeEventManager); - stripeClient = module.get(StripeClient); + stripeWebhooksController = module.get(StripeWebhooksController); + stripeWebhookService = module.get(StripeWebhookService); }); - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('SubscriptionEventsService', () => { - describe('constructWebhookEventResponse', () => { - it('customer.subscription.deleted - returns subscription object', () => { - jest - .spyOn(stripeClient, 'constructWebhookEvent') - .mockReturnValue(StripeEventCustomerSubscriptionDeletedFactory()); - const result = stripeEventManager.constructWebhookEventResponse({}, ''); - expect(result.type).toBe('customer.subscription.deleted'); - }); + describe('postStripe', () => { + beforeEach(() => { + jest + .spyOn(stripeWebhookService, 'handleWebhookEvent') + .mockResolvedValue({}); + }); - it('customer.subscription.created - returns default', () => { - jest - .spyOn(stripeClient, 'constructWebhookEvent') - .mockReturnValue(StripeEventCustomerSubscriptionCreatedFactory()); - const result = stripeEventManager.constructWebhookEventResponse({}, ''); - expect(result.type).toBe('customer.subscription.created'); - }); + it('successfully processes a request', async () => { + await stripeWebhooksController.postStripe('test' as any, 'test'); + expect(stripeWebhookService.handleWebhookEvent).toHaveBeenCalled(); }); }); }); diff --git a/libs/payments/webhooks/src/lib/stripe-webhooks.controller.ts b/libs/payments/webhooks/src/lib/stripe-webhooks.controller.ts new file mode 100644 index 00000000000..db5d488cb0b --- /dev/null +++ b/libs/payments/webhooks/src/lib/stripe-webhooks.controller.ts @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + Controller, + Headers, + HttpCode, + Post, + Req, + type RawBodyRequest, +} from '@nestjs/common'; +import { StripeWebhookService } from './stripe-webhooks.service'; + +@Controller('webhooks') +export class StripeWebhooksController { + constructor(private stripeWebhookService: StripeWebhookService) {} + + @Post('stripe') + @HttpCode(200) + async postStripe( + @Req() req: RawBodyRequest, + @Headers('stripe-signature') signature: string + ) { + await this.stripeWebhookService.handleWebhookEvent(req.rawBody, signature); + + return 'ok'; + } +} diff --git a/libs/payments/webhooks/src/lib/stripeWebhooks.service.spec.ts b/libs/payments/webhooks/src/lib/stripe-webhooks.service.spec.ts similarity index 79% rename from libs/payments/webhooks/src/lib/stripeWebhooks.service.spec.ts rename to libs/payments/webhooks/src/lib/stripe-webhooks.service.spec.ts index c48db9611da..6d621619f46 100644 --- a/libs/payments/webhooks/src/lib/stripeWebhooks.service.spec.ts +++ b/libs/payments/webhooks/src/lib/stripe-webhooks.service.spec.ts @@ -5,10 +5,10 @@ import { Test } from '@nestjs/testing'; import { MockStripeConfigProvider, StripeClient } from '@fxa/payments/stripe'; -import { StripeEventManager } from './stripeEvents.manager'; -import { StripeWebhookService } from './stripeWebhooks.service'; +import { StripeEventManager } from './stripe-event.manager'; +import { StripeWebhookService } from './stripe-webhooks.service'; import { CustomerSubscriptionDeletedResponseFactory } from './factories'; -import { SubscriptionEventsService } from './subscriptionHandler.service'; +import { SubscriptionEventsService } from './subscription-handler.service'; import { CustomerManager, InvoiceManager, @@ -42,6 +42,7 @@ import { MockStatsDProvider } from '@fxa/shared/metrics/statsd'; import { MockAccountDatabaseNestFactory } from '@fxa/shared/db/mysql/account'; import * as Sentry from '@sentry/node'; import { Logger } from '@nestjs/common'; +import { MockStripeEventConfigProvider } from './stripe-event.config'; jest.mock('@sentry/node', () => ({ captureException: jest.fn(), @@ -69,9 +70,10 @@ describe('StripeWebhookService', () => { }, { provide: PaymentMethodManager, - useValue: paymentMethodManagerMock + useValue: paymentMethodManagerMock, }, MockStripeConfigProvider, + MockStripeEventConfigProvider, StripeClient, StripeEventManager, StripeWebhookService, @@ -116,6 +118,10 @@ describe('StripeWebhookService', () => { jest .spyOn(stripeEventManager, 'constructWebhookEventResponse') .mockReturnValue(CustomerSubscriptionDeletedResponseFactory()); + jest.spyOn(stripeEventManager, 'isProcessed').mockResolvedValue(false); + jest + .spyOn(stripeEventManager, 'markAsProcessed') + .mockResolvedValue(undefined); }); describe('handleWebhookEvent', () => { @@ -125,6 +131,17 @@ describe('StripeWebhookService', () => { expect( (stripeWebhookService as any).dispatchEventToHandler ).toHaveBeenCalled(); + expect(stripeEventManager.markAsProcessed).toHaveBeenCalled(); + }); + + it('should return early if event already processed', async () => { + jest.spyOn(stripeEventManager, 'isProcessed').mockResolvedValue(true); + + await stripeWebhookService.handleWebhookEvent({}, 'signature'); + expect( + (stripeWebhookService as any).dispatchEventToHandler + ).not.toHaveBeenCalled(); + expect(stripeEventManager.markAsProcessed).not.toHaveBeenCalled(); }); it('should report exception to Sentry and throw on exception', async () => { diff --git a/libs/payments/webhooks/src/lib/stripeWebhooks.service.ts b/libs/payments/webhooks/src/lib/stripe-webhooks.service.ts similarity index 75% rename from libs/payments/webhooks/src/lib/stripeWebhooks.service.ts rename to libs/payments/webhooks/src/lib/stripe-webhooks.service.ts index f5b9f16e11f..33025504b2b 100644 --- a/libs/payments/webhooks/src/lib/stripeWebhooks.service.ts +++ b/libs/payments/webhooks/src/lib/stripe-webhooks.service.ts @@ -4,10 +4,10 @@ import { Injectable } from '@nestjs/common'; -import { StripeEventManager } from './stripeEvents.manager'; +import { StripeEventManager } from './stripe-event.manager'; import { StripeWebhookEventResponse } from './types'; import * as Sentry from '@sentry/nestjs'; -import { SubscriptionEventsService } from './subscriptionHandler.service'; +import { SubscriptionEventsService } from './subscription-handler.service'; @Injectable() export class StripeWebhookService { @@ -28,11 +28,23 @@ export class StripeWebhookService { payload, signature ); + + const eventAlreadyProcessed = await this.stripeEventManager.isProcessed( + webhookEventResponse.event.id + ); + + if (eventAlreadyProcessed) { + return {}; + } + await this.dispatchEventToHandler(webhookEventResponse); + + await this.stripeEventManager.markAsProcessed(webhookEventResponse.event); } catch (error) { Sentry.captureException(error); throw error; } + return {}; } @@ -47,6 +59,7 @@ export class StripeWebhookService { ); break; default: + console.log('DEFAULT EVENT HANDLER', webhookResponse.type); } } } diff --git a/libs/payments/webhooks/src/lib/stripeEvents.manager.ts b/libs/payments/webhooks/src/lib/stripeEvents.manager.ts deleted file mode 100644 index 7a0079ac5dc..00000000000 --- a/libs/payments/webhooks/src/lib/stripeEvents.manager.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { StripeClient } from '@fxa/payments/stripe'; -import { Injectable } from '@nestjs/common'; -import Stripe from 'stripe'; -import { StripeWebhookEventResponse } from './types'; - -@Injectable() -export class StripeEventManager { - constructor(private stripeClient: StripeClient) {} - - constructWebhookEventResponse( - payload: any, - signature: string - ): StripeWebhookEventResponse { - const stripeEvent = this.stripeClient.constructWebhookEvent( - payload, - signature - ); - - switch (stripeEvent.type) { - case 'customer.subscription.deleted': - return { - type: 'customer.subscription.deleted', - event: stripeEvent, - eventObjectData: stripeEvent.data.object as Stripe.Subscription, - }; - default: - return { - type: stripeEvent.type, - event: stripeEvent, - eventObjectData: stripeEvent.data.object, - }; - } - } -} diff --git a/libs/payments/webhooks/src/lib/subscriptionHandler.service.spec.ts b/libs/payments/webhooks/src/lib/subscription-handler.service.spec.ts similarity index 94% rename from libs/payments/webhooks/src/lib/subscriptionHandler.service.spec.ts rename to libs/payments/webhooks/src/lib/subscription-handler.service.spec.ts index 33ec719e962..41b0c06ddd0 100644 --- a/libs/payments/webhooks/src/lib/subscriptionHandler.service.spec.ts +++ b/libs/payments/webhooks/src/lib/subscription-handler.service.spec.ts @@ -12,9 +12,9 @@ import { StripeResponseFactory, StripeSubscriptionFactory, } from '@fxa/payments/stripe'; -import { StripeEventManager } from './stripeEvents.manager'; -import { StripeWebhookService } from './stripeWebhooks.service'; -import { SubscriptionEventsService } from './subscriptionHandler.service'; +import { StripeEventManager } from './stripe-event.manager'; +import { StripeWebhookService } from './stripe-webhooks.service'; +import { SubscriptionEventsService } from './subscription-handler.service'; import { CustomerDeletedError, CustomerManager, @@ -54,6 +54,7 @@ import { determineCancellation, } from './util/determineCancellation'; import { Logger } from '@nestjs/common'; +import { MockStripeEventConfigProvider } from './stripe-event.config'; jest.mock('@fxa/payments/customer'); jest.mock('./util/determineCancellation'); @@ -90,9 +91,10 @@ describe('SubscriptionEventsService', () => { }, { provide: PaymentMethodManager, - useValue: paymentMethodManagerMock + useValue: paymentMethodManagerMock, }, MockStripeConfigProvider, + MockStripeEventConfigProvider, StripeClient, StripeEventManager, StripeWebhookService, @@ -170,9 +172,11 @@ describe('SubscriptionEventsService', () => { }); it('should emit the subscriptionEnded event, with paymentProvider external_paypal', async () => { - (paymentMethodManager.determineType as jest.Mock).mockResolvedValueOnce({ - type: SubPlatPaymentMethodType.PayPal, - }); + (paymentMethodManager.determineType as jest.Mock).mockResolvedValueOnce( + { + type: SubPlatPaymentMethodType.PayPal, + } + ); await subscriptionEventsService.handleCustomerSubscriptionDeleted( mockEvent, mockEventObjectData @@ -242,7 +246,7 @@ describe('SubscriptionEventsService', () => { it('should emit the subscriptionEnded event, with paymentProvider undefined on error', async () => { jest .spyOn(customerManager, 'retrieve') - .mockRejectedValue(new CustomerDeletedError()); + .mockRejectedValue(new CustomerDeletedError('customerId')); await subscriptionEventsService.handleCustomerSubscriptionDeleted( mockEvent, mockEventObjectData diff --git a/libs/payments/webhooks/src/lib/subscriptionHandler.service.ts b/libs/payments/webhooks/src/lib/subscription-handler.service.ts similarity index 92% rename from libs/payments/webhooks/src/lib/subscriptionHandler.service.ts rename to libs/payments/webhooks/src/lib/subscription-handler.service.ts index 1fa4d9ef743..ee77fde37f5 100644 --- a/libs/payments/webhooks/src/lib/subscriptionHandler.service.ts +++ b/libs/payments/webhooks/src/lib/subscription-handler.service.ts @@ -25,7 +25,7 @@ export class SubscriptionEventsService { private customerManager: CustomerManager, private invoiceManager: InvoiceManager, private emitterService: PaymentsEmitterService, - private paymentMethodManager: PaymentMethodManager, + private paymentMethodManager: PaymentMethodManager ) {} async handleCustomerSubscriptionDeleted( @@ -45,13 +45,15 @@ export class SubscriptionEventsService { subscription.customer ); uid = customer.metadata['userid']; - const paymentMethodType = await this.paymentMethodManager.determineType(customer, [ - subscription, - ]); + const paymentMethodType = await this.paymentMethodManager.determineType( + customer, + [subscription] + ); paymentProvider = paymentMethodType?.type; const latestInvoice = - paymentProvider === SubPlatPaymentMethodType.PayPal && subscription.latest_invoice + paymentProvider === SubPlatPaymentMethodType.PayPal && + subscription.latest_invoice ? await this.invoiceManager.retrieve(subscription.latest_invoice) : undefined; diff --git a/libs/shared/cms/src/lib/strapi.client.config.ts b/libs/shared/cms/src/lib/strapi.client.config.ts index fc390c5ae21..ec674b11166 100644 --- a/libs/shared/cms/src/lib/strapi.client.config.ts +++ b/libs/shared/cms/src/lib/strapi.client.config.ts @@ -4,6 +4,7 @@ import { faker } from '@faker-js/faker'; import { Provider } from '@nestjs/common'; +import { Type } from 'class-transformer'; import { IsNumber, IsString, IsUrl } from 'class-validator'; export class StrapiClientConfig { @@ -13,15 +14,18 @@ export class StrapiClientConfig { @IsString() public readonly apiKey!: string; + @Type(() => Number) @IsNumber() public readonly memCacheTTL?: number; @IsString() public readonly firestoreCacheCollectionName!: string; + @Type(() => Number) @IsNumber() public readonly firestoreCacheTTL?: number; + @Type(() => Number) @IsNumber() public readonly firestoreOfflineCacheTTL?: number; } diff --git a/libs/shared/db/firestore/src/lib/firestore.provider.ts b/libs/shared/db/firestore/src/lib/firestore.provider.ts index 456ed553c56..4658898bb3f 100644 --- a/libs/shared/db/firestore/src/lib/firestore.provider.ts +++ b/libs/shared/db/firestore/src/lib/firestore.provider.ts @@ -70,7 +70,9 @@ export const FirestoreProvider: Provider = { export const MockFirestoreProvider: Provider = { provide: FirestoreService, useFactory: () => { - return {} as Firestore; + return { + collection: () => {}, + } as unknown as Firestore; }, }; diff --git a/libs/shared/metrics/statsd/src/index.ts b/libs/shared/metrics/statsd/src/index.ts index 0d98b584133..125298e3640 100644 --- a/libs/shared/metrics/statsd/src/index.ts +++ b/libs/shared/metrics/statsd/src/index.ts @@ -3,5 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ export { StatsD } from 'hot-shots'; export * from './lib/statsd'; +export * from './lib/statsd.config'; export * from './lib/statsd.provider'; export * from './lib/statsd.decorator'; diff --git a/libs/shared/metrics/statsd/src/lib/statsd.config.ts b/libs/shared/metrics/statsd/src/lib/statsd.config.ts index 84825aa3d36..6e7edce0b17 100644 --- a/libs/shared/metrics/statsd/src/lib/statsd.config.ts +++ b/libs/shared/metrics/statsd/src/lib/statsd.config.ts @@ -2,12 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { IsInt, IsOptional, IsString } from 'class-validator'; +import { Type } from 'class-transformer'; export class StatsDConfig { + @Type(() => Number) @IsInt() @IsOptional() sampleRate?: number; + @Type(() => Number) @IsInt() @IsOptional() maxBufferSize?: number; @@ -16,6 +19,7 @@ export class StatsDConfig { @IsOptional() host?: string; + @Type(() => Number) @IsInt() @IsOptional() port?: number;