DEV Community

Mofajjal Rasul
Mofajjal Rasul

Posted on

Integrating Stripe Payment Intent in NestJS with Webhook Handling

In this article, we'll walk through how to implement Stripe payment processing in a NestJS application, focusing on creating a payment intent for bookings and handling webhook events from Stripe. We'll cover the controller, service, and the business logic involved in interacting with Stripe's API, including updating the booking status based on Stripe's payment events.

1. Setup Nest Application With Express

To properly handle Stripe webhooks in a NestJS application, one key aspect to ensure smooth processing is retaining the raw request body. This is particularly important for webhook signature verification. To prevent NestJS from parsing the body, you must set rawBody: true in the application setup. This ensures the request body remains in its raw form, allowing Stripe's signature verification process to function properly.

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule, {
    rawBody: true,  // Retain raw request body for Stripe webhook verification
  });


    // ...
}
Enter fullscreen mode Exit fullscreen mode

2. Setting Up the Controller

The controller handles incoming HTTP requests related to payment processing. We define two routes: one for retrieving a payment intent based on a booking ID and another for handling webhook events sent by Stripe.

@ApiTags("Payment Receive")
@Controller("PaymentReceive")
export class PaymentReceiveController {
  constructor(private readonly paymentService: PaymentReceiveService) {}

  @Get("GetIntentByBookingId/:DocId")
  @ApiResponse({ status: 200, type: SuccessResponseDto })
  getPaymentIntent(
    @AuthUserId() { userId }: ITokenPayload,
    @Param() { DocId }: DocIdQueryDto,
  ) {
    return this.paymentService.getPaymentIntent(DocId, userId);
  }

  @Post("StripeWebhook")
  @HttpCode(200)
  @IsPublic()
  @ApiExcludeEndpoint()
  async handleStripeWebhook(
    @Req() req: RawBodyRequest<Request>,
    @Headers("stripe-signature") signature: string,
  ): Promise<void> {
    await this.paymentService.handleStripeWebhook(req, signature);
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Get: GetIntentByBookingId/:DocId: This route fetches the payment intent associated with a booking by its DocId. The AuthUserId decorator ensures that the user is authenticated.
  • Post: StripeWebhook: This route handles the incoming webhook notifications from Stripe. The @IsPublic() decorator allows this route to be accessed without authentication since webhooks are sent by Stripe, not the user. The @ApiExcludeEndpoint() decorator hides this endpoint for swagger doc schema. The @Req() req: RawBodyRequest<Request> params provides raw request body from stripe webhook.

3. The Payment Receive Service

This service handles the business logic of creating and retrieving payment intents from Stripe, as well as processing the events sent via Stripe webhooks. The key logic revolves around managing payment statuses and updating related entities in the database.

@Injectable()
export class StripePaymentService {
  private readonly logger: Logger = new Logger(StripePaymentService.name);
  private readonly stripeService: Stripe;

  private readonly stripeWebhookSecret: string;
  private readonly stripePublishableKey: string;

  constructor(
    private readonly configService: ConfigService,
  ) {
    const stripeSecretKey = this.configService.get<string>("STRIPE_SECRET_KEY", "");
    this.stripePublishableKey = this.configService.get<string>("STRIPE_PUBLISHABLE_KEY", "");
    this.stripeWebhookSecret = this.configService.get<string>("STRIPE_WEBHOOK_SECRET", "");

    this.stripeService = new Stripe(stripeSecretKey, {});
  }

  async getPaymentIntent(
    bookingId: string,
    auditUserId: string,
  ): Promise<SuccessResponseDto> {
    try {
      // Here you retrieve the booking by its DocId (e.g., bookingId)
      // If booking is not found, throw an exception (e.g., NotFoundException)

      let stripeIntent: Stripe.PaymentIntent;

      // Here, check if a payment intent already exists for the booking
      // If not, create a new payment intent via Stripe

      const intentMetadata: StripeIntentMetadata = {
        bookingCode: "bookingCode", // Example: Booking code should be passed dynamically
        bookingId: bookingId,
        paymentReceiveId: "paymentReceiveId", // Save paymentReceiveId dynamically
      };

      // Create a payment intent with Stripe
      stripeIntent = await this.stripeService.paymentIntents.create({
        amount: 1000, // Example: amount should be the booking total price in cents
        currency: "usd", // Set currency
        metadata: intentMetadata,
      });

      // Here you save the payment intent details (e.g., stripeIntent) to your database

      const paymentIntentResponse = new PaymentIntentResponseDto();
      paymentIntentResponse.stripeKey = this.stripePublishableKey;
      paymentIntentResponse.stripeSecret = stripeIntent.client_secret || "";
      paymentIntentResponse.status = stripeIntent.status;
      paymentIntentResponse.currency = stripeIntent.currency;

      return new SuccessResponseDto(
        "Payment intent retrieved successfully",
        paymentIntentResponse,
      );
    } catch (error) {
      this.logger.error("Error in getPaymentIntent:", error);
      throw new BadRequestException("Failed to get payment intent");
    }
  }

  async handleStripeWebhook(
    request: RawBodyRequest<Request>,
    signature: string,
  ) {
    try {
      if (!this.stripeWebhookSecret) {
        throw new Error("Stripe webhook secret not found in the configuration.");
      }

      const event = this.stripeService.webhooks.constructEvent(
        request.rawBody as Buffer,
        signature,
        this.stripeWebhookSecret,
      );

      // Handle the different event types sent by Stripe
      switch (event.type) {
        case "payment_intent.created":
          // Here you handle the payment intent created event
          // Example: Update booking status to 'Payment Created'
          break;

        case "payment_intent.succeeded":
          // Here you handle the successful payment event
          // Example: Update booking status to 'Payment Completed'
          break;

        case "payment_intent.payment_failed":
          // Here you handle the failed payment event
          // Example: Update booking status to 'Payment Failed'
          break;

        default:
          this.logger.log("Unhandled event type: " + event.type);
      }

      // Here you update the database based on the event
      // For example, update payment status in your records

      return { received: true };
    } catch (error) {
      this.logger.error("Error handling Stripe webhook event:", error);
      throw new BadRequestException("Error in webhook event processing");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • getPaymentIntent: This function checks if a payment intent already exists for the booking. If not, it creates a new payment intent in Stripe. It also updates the booking status and payment record in the database.
  • handleStripeWebhook: This method processes incoming webhook events from Stripe. It updates the payment status and booking status based on the event type, such as payment success or failure.

4. The PaymentReceiveModule

In a typical NestJS application, we would include the necessary modules for the database connection and business logic. However, the imports section for the PaymentReceiveModule has been omitted here for clarity, as the focus is on the core logic of payment intent and webhook handling.

@Module({
  controllers: [PaymentReceiveController],
  providers: [PaymentReceiveService, PaymentReceiveRepository],
})
export class PaymentReceiveModule {}
Enter fullscreen mode Exit fullscreen mode

Key Notes:

  • The PaymentReceiveService is injected into the controller to handle all business logic related to payment processing.
  • We use Stripe’s official Node.js SDK to interact with Stripe’s API, managing payment intents and webhook events.
  • The service includes robust error handling, logging, and status updates to ensure smooth operation in a production environment.

Conclusion

This implementation provides a clean, professional way to handle Stripe payments in a NestJS application. By managing payment intents and handling webhooks efficiently, we ensure that users can process payments securely, and booking statuses remain synchronized with payment updates.

Top comments (0)