From 8f54754abaa58a5af2e638b816e8d1cffa0789d7 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 1 Dec 2025 13:24:19 +0200 Subject: [PATCH] feat: add stripe payment webhooks controller --- .../queries/GetPaymentMethodsState.ts | 2 +- .../StripePayment/StripePayment.module.ts | 5 +- .../StripePaymentWebhooks.controller.ts | 124 ++++++++++++++++++ 3 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 packages/server/src/modules/StripePayment/StripePaymentWebhooks.controller.ts diff --git a/packages/server/src/modules/PaymentServices/queries/GetPaymentMethodsState.ts b/packages/server/src/modules/PaymentServices/queries/GetPaymentMethodsState.ts index 98200fd5c..9af65ee8d 100644 --- a/packages/server/src/modules/PaymentServices/queries/GetPaymentMethodsState.ts +++ b/packages/server/src/modules/PaymentServices/queries/GetPaymentMethodsState.ts @@ -15,7 +15,7 @@ export class GetPaymentMethodsStateService { private readonly paymentIntegrationModel: TenantModelProxy< typeof PaymentIntegration >, - ) {} + ) { } /** * Retrieves the payment state provising state. diff --git a/packages/server/src/modules/StripePayment/StripePayment.module.ts b/packages/server/src/modules/StripePayment/StripePayment.module.ts index 45d5a9b16..503949970 100644 --- a/packages/server/src/modules/StripePayment/StripePayment.module.ts +++ b/packages/server/src/modules/StripePayment/StripePayment.module.ts @@ -6,6 +6,7 @@ import { ExchangeStripeOAuthTokenService } from './ExchangeStripeOauthToken'; import { SeedStripeAccountsOnOAuthGrantedSubscriber } from './subscribers/SeedStripeAccounts'; import { StripeWebhooksSubscriber } from './subscribers/StripeWebhooksSubscriber'; import { StripeIntegrationController } from './StripePayment.controller'; +import { StripePaymentWebhooksController } from './StripePaymentWebhooks.controller'; import { StripePaymentService } from './StripePaymentService'; import { GetStripeAuthorizationLinkService } from './GetStripeAuthorizationLink'; import { AccountsModule } from '../Accounts/Accounts.module'; @@ -33,6 +34,6 @@ import { TenancyContext } from '../Tenancy/TenancyContext.service'; TenancyContext, ], exports: [StripePaymentService, GetStripeAuthorizationLinkService], - controllers: [StripeIntegrationController], + controllers: [StripeIntegrationController, StripePaymentWebhooksController], }) -export class StripePaymentModule {} +export class StripePaymentModule { } diff --git a/packages/server/src/modules/StripePayment/StripePaymentWebhooks.controller.ts b/packages/server/src/modules/StripePayment/StripePaymentWebhooks.controller.ts new file mode 100644 index 000000000..c8d5bb6fd --- /dev/null +++ b/packages/server/src/modules/StripePayment/StripePaymentWebhooks.controller.ts @@ -0,0 +1,124 @@ +import { + Controller, + Headers, + HttpCode, + HttpException, + HttpStatus, + Post, + Req, + Res, +} from '@nestjs/common'; +import { Request, Response } from 'express'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { ConfigService } from '@nestjs/config'; +import { StripePaymentService } from './StripePaymentService'; +import { events } from '@/common/events/events'; +import { + StripeCheckoutSessionCompletedEventPayload, + StripeWebhookEventPayload, +} from './StripePayment.types'; +import { PublicRoute } from '../Auth/guards/jwt.guard'; + +@Controller('/webhooks/stripe') +@ApiTags('stripe') +@PublicRoute() +export class StripePaymentWebhooksController { + constructor( + private readonly stripePaymentService: StripePaymentService, + private readonly eventEmitter: EventEmitter2, + private readonly configService: ConfigService, + ) { } + + /** + * Handles incoming Stripe webhook events. + * Verifies the webhook signature, processes the event based on its type, + * and triggers appropriate actions or events in the system. + * @param {Request} req - The Express request object containing the webhook payload. + * @param {Response} res - The Express response object. + * @returns {Promise} + */ + @Post('/') + @HttpCode(200) + @ApiOperation({ summary: 'Listen to Stripe webhooks' }) + async handleWebhook( + @Req() req: Request, + @Res() res: Response, + @Headers('stripe-signature') signature: string, + ) { + console.log(signature, 'signature'); + try { + // @ts-ignore - rawBody is set by middleware + const rawBody = req.rawBody || req.body; + const webhooksSecret = this.configService.get( + 'stripePayment.webhooksSecret', + ); + if (!webhooksSecret) { + throw new HttpException( + 'Stripe webhook secret is not configured', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + if (!signature) { + throw new HttpException( + 'Stripe signature header is missing', + HttpStatus.BAD_REQUEST, + ); + } + let event; + + // Verify webhook signature and extract the event. + // See https://stripe.com/docs/webhooks#verify-events for more information. + try { + event = this.stripePaymentService.stripe.webhooks.constructEvent( + rawBody, + signature, + webhooksSecret, + ); + } catch (err) { + throw new HttpException( + `Webhook Error: ${err.message}`, + HttpStatus.BAD_REQUEST, + ); + } + console.log(event.type, 'event.type'); + // Handle the event based on its type + switch (event.type) { + case 'checkout.session.completed': + // Triggers `onStripeCheckoutSessionCompleted` event. + await this.eventEmitter.emitAsync( + events.stripeWebhooks.onCheckoutSessionCompleted, + { + event, + } as StripeCheckoutSessionCompletedEventPayload, + ); + break; + + case 'account.updated': + // Triggers `onStripeAccountUpdated` event. + await this.eventEmitter.emitAsync( + events.stripeWebhooks.onAccountUpdated, + { + event, + } as StripeWebhookEventPayload, + ); + break; + + // Add more cases as needed + default: + console.log(`Unhandled event type ${event.type}`); + } + + return res.status(200).json({ received: true }); + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +}