DEV Community

Jayvee Ramos
Jayvee Ramos

Posted on

13. Integrating and Validating Payment Services in a Reservation System

Introduction:

In this tutorial, we will walk you through the process of integrating a payment service with a reservation service. This integration will enable your application to bill users for reservations. We will also cover the validation of input data to ensure data consistency and security.


Step 1: Define Payment Service

Define PAYMENTS_SERVICE in your libs/common/src/constants/service.ts

service.ts

export const AUTH_SERVICE = 'auth';
export const PAYMENTS_SERVICE = 'payments';
Enter fullscreen mode Exit fullscreen mode

Step 2: Modify Reservation Module

To enable the reservation service to communicate with the payment service, we first need to set up the payment service as a client. In your reservations module, inject the payment service using an injection token:

import { Module } from '@nestjs/common';
import { ReservationsService } from './reservations.service';
import { ReservationsController } from './reservations.controller';
import {
  AUTH_SERVICE,
  DatabaseModule,
  LoggerModule,
  PAYMENTS_SERVICE,
} from '@app/common';
import { ReservationsRepository } from './reservations.repository';
import {
  ReservationDocument,
  ReservationSchema,
} from './entities/reservation.entity';
import { ConfigModule, ConfigService } from '@nestjs/config';
import * as Joi from 'joi';
import { ClientsModule, Transport } from '@nestjs/microservices';

@Module({
  imports: [
    DatabaseModule,
    DatabaseModule.forFeature([
      { name: ReservationDocument.name, schema: ReservationSchema },
    ]),
    LoggerModule,
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        MONGODB_URI: Joi.string().required(),
        PORT: Joi.number().required(),
        AUTH_HOST: Joi.string().required(),
        AUTH_PORT: Joi.number().required(),
      }),
    }),
    ClientsModule.registerAsync([
      {
        name: AUTH_SERVICE,
        useFactory: (configService: ConfigService) => ({
          transport: Transport.TCP,
          options: {
            host: configService.get('AUTH_HOST'),
            port: configService.get('AUTH_PORT'),
          },
        }),
        inject: [ConfigService],
      },
      {
        name: PAYMENTS_SERVICE,
        useFactory: (configService: ConfigService) => ({
          transport: Transport.TCP,
          options: {
            host: configService.get('PAYMENTS_HOST'),
            port: configService.get('PAYMENTS_PORT'),
          },
        }),
        inject: [ConfigService],
      },
    ]),
  ],
  controllers: [ReservationsController],
  providers: [ReservationsService, ReservationsRepository],
})
export class ReservationsModule {}

Enter fullscreen mode Exit fullscreen mode

Step 3: Configure Environment Variables

Since we are using two new environment variables, PAYMENTS_HOST and PAYMENTS_PORT, which do not yet exist in our .env file, let's go ahead and define them. Please open your payments .env file and modify its content with the following code.

MONGODB_URI=mongodb://mongo:27017/reservation
PORT=3000
AUTH_HOST=auth
AUTH_PORT=3002
PAYMENTS_HOST=payments
PAYMENTS_PORT=3003
Enter fullscreen mode Exit fullscreen mode

Step 4: Inject the Payment Service

Inject the payment service using NestJS's @Inject decorator and call it as a client proxy for microservices. Ensure that the import statement is correct.
update your reservations.service.ts with this content:

import { Inject, Injectable } from '@nestjs/common';

import { ReservationsRepository } from './reservations.repository';
import { CreateReservationDto } from './dto/create-reservation.dto';
import { UpdateReservationDto } from './dto/update-reservation.dto';
import { PAYMENTS_SERVICE } from '@app/common';
import { ClientProxy } from '@nestjs/microservices';

@Injectable()
export class ReservationsService {
  @Inject(PAYMENTS_SERVICE) payment_service: ClientProxy;
  constructor(
    private readonly reservationsRepository: ReservationsRepository,
  ) {}

  async create(createReservationDto: CreateReservationDto, userId: string) {
    return this.reservationsRepository.create({
      ...createReservationDto,
      timestamp: new Date(),
      userId,
    });
  }

  async findAll() {
    return this.reservationsRepository.find({});
  }

  async findOne(_id: string) {
    return this.reservationsRepository.findOne({ _id });
  }

  async update(_id: string, updateReservationDto: UpdateReservationDto) {
    return this.reservationsRepository.findOneAndUpdate(
      { _id },
      { $set: updateReservationDto },
    );
  }

  async remove(_id: string) {
    return this.reservationsRepository.findOneAndDelete({ _id });
  }
}

Enter fullscreen mode Exit fullscreen mode

This step allows you to use the payment service for billing users when creating reservations.


Step 5: Creating Card DTO

Create a separate CardDTO to validate the credit card details. We will do it inside our libs/common/src/dto folder to allow us to use the CardDTO anywhere in our apps to avoid duplication:

touch libs/common/src/dto/card.dto.ts
touch libs/common/src/dto/create-charge.dto.ts
Enter fullscreen mode Exit fullscreen mode

populate your card.dto.ts with the following content:

import { IsCreditCard, IsNotEmpty, IsNumber, IsString } from 'class-validator';

export class CardDTO {
  @IsString()
  @IsNotEmpty()
  cvc: string;

  @IsNumber()
  exp_month: number;

  @IsNumber()
  exp_year: number;

  @IsCreditCard()
  number: string;
}

Enter fullscreen mode Exit fullscreen mode

populate your create-charge.dto.ts with the following content:

import { CardDTO } from './card.dto';
import { Type } from 'class-transformer';
import {
  IsDefined,
  IsNotEmpty,
  ValidateNested,
  IsNumber,
} from 'class-validator';

export class CreateChargeDto {
  @Type(() => CardDTO)
  @IsDefined()
  @IsNotEmpty()
  @ValidateNested()
  card: CardDTO;

  @IsNumber()
  amount: number;
}


Enter fullscreen mode Exit fullscreen mode

Make sure to update the libs/common/src/dto/index.ts and export the newly created CardDTO and CreateChargeDto

export * from './user.dto';
export * from './card.dto';
export * from './create-charge.dto';
Enter fullscreen mode Exit fullscreen mode

Please remember that we have already created a 'create-charge.dto' in our payments app. We need to delete the existing one and replace it with the newly created 'create-charge.dto' located inside our common folder. This change is necessary because we have not applied any validation to the 'create-charge.dto' in our payments app.

update your paymens.controller.ts with the following content to use our newly created create-charge.dto.ts in our @app/common:

import { Controller } from '@nestjs/common';
import { PaymentsService } from './payments.service';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { CreateChargeDto } from '@app/common';

@Controller()
export class PaymentsController {
  constructor(private readonly paymentsService: PaymentsService) {}

  @MessagePattern('create_charge')
  async createcharge(@Payload() data: CreateChargeDto) {
    return this.paymentsService.createCharge(data);
  }
}

Enter fullscreen mode Exit fullscreen mode

Step 6: Updating the Create Reservation DTO

To facilitate billing for reservations, update the Create Reservation DTO to include a card property.

In your reservation app inside the dto folder, Open your create-reservation.dto.ts file and update with this content:

import { CardDTO } from '@app/common';
import { Type } from 'class-transformer';
import {
  IsDate,
  IsDefined,
  IsNotEmpty,
  IsNumber,
  IsString,
  ValidateNested,
} from 'class-validator';

export class CreateReservationDto {
  @IsDate()
  @Type(() => Date)
  startDate: Date;

  @IsDate()
  @Type(() => Date)
  endDate: Date;

  @IsString()
  @IsNotEmpty()
  placeId: string;

  @IsString()
  @IsNotEmpty()
  invoiceId: string;

  @Type(() => CardDTO)
  @IsDefined()
  @IsNotEmpty()
  @ValidateNested()
  card: CardDTO;

  @IsNumber()
  amount: number;
}

Enter fullscreen mode Exit fullscreen mode

Step 7: Test Your DTO

Test Scenario: Reservation Creation with Validation
Request Details:

Method: POST
Endpoint: http://localhost:3000/reservations
Test Steps:

  1. Invalid Card Information missing cvc

Send a POST request with the following payload:

{
  "startDate": "2023-11-01",
  "endDate": "2023-11-05",
  "placeId": "1232",
  "invoiceId": "67890",
  "charge": {
   "card": {
    "exp_month": 12,     
    "exp_year": 2025,      
    "number": "4242 4242 4242 4242"  
  },
  "amount": 99.99
  }
}

Enter fullscreen mode Exit fullscreen mode

Check the response for validation errors, especially for the "cvc" field.

  1. Valid Card Information

Send a POST request with the following payload:

{
  "startDate": "2023-11-01",
  "endDate": "2023-11-05",
  "placeId": "1232",
  "invoiceId": "67890",
  "charge": {
   "card": {
    "cvc": "123",
    "exp_month": 12,     
    "exp_year": 2025,      
    "number": "4242 4242 4242 4242"  
  },
  "amount": 99.99
  }
}

Enter fullscreen mode Exit fullscreen mode

Check the response for a successful reservation creation.

Expected Outcomes:

In Step 1, you should receive a validation error response indicating that the "cvc" field is incorrect.

In Step 2, you should receive a successful reservation creation response.

This scenario will help you verify if the validation is working correctly and if the reservation creation is functioning as expected.


Conclusion:

You've successfully integrated and validated the payment service in your reservation system. With robust schema validation and proper data exchange, you can ensure a reliable and secure booking process. This tutorial provides a foundation for building more complex systems and enhancing the capabilities of your microservices. Happy coding!

In the next lesson, we will connect it with our Stripe to persist our payments and make a few modifications to our payment service. This will be our last lesson in payment integration.

Top comments (0)