diff --git a/.env.example b/.env.example index 6d963a7..452d934 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,5 @@ +POSTGRES_DB= +POSTGRES_USER= +POSTGRES_PASSWORD= DATABASE_URL= PORT = 4000 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2c07246 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# Use the official Node.js image as the base image +FROM node:16 + +# Set the working directory inside the container +WORKDIR /app + +# Copy package.json and package-lock.json to the working directory +COPY package*.json ./ + +# Install project dependencies +RUN npm install + +# Copy the rest of the application code to the container +COPY . . + +# Expose the PORT environment variable (default to 4000 if not provided) +ARG PORT=4000 +ENV PORT=$PORT + +# Build your Nest.js application +RUN npm run build + +# Start the Nest.js application using the start:prod script +CMD ["npm", "run", "start:prod"] diff --git a/docker-compose.yml b/docker-compose.yml index e69de29..1d6ace5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +version: '3' +services: + app: + build: + context: . + dockerfile: Dockerfile + image: marketplace-request-service + container_name: marketplace + environment: + - PORT=${PORT:-4000} + ports: + - '${PORT:-4000}:4000' + depends_on: + - db + networks: + - nest-network + + db: + image: postgres:13 + container_name: postgres-container + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + ports: + - '5432:5432' + volumes: + - pg-data:/var/lib/postgresql/data + networks: + - nest-network + +volumes: + pg-data: + +networks: + nest-network: diff --git a/package-lock.json b/package-lock.json index e103b20..f04d866 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^7.1.11", "@prisma/client": "^5.3.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0" @@ -2141,6 +2143,11 @@ "@types/superagent": "*" } }, + "node_modules/@types/validator": { + "version": "13.11.1", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.1.tgz", + "integrity": "sha512-d/MUkJYdOeKycmm75Arql4M5+UuXmf4cHdHKsyw1GcvnNgL6s77UkgSgJ8TE/rI5PYsnwYq5jkcWBLuN/MpQ1A==" + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -3145,6 +3152,21 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "node_modules/class-validator": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz", + "integrity": "sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==", + "dependencies": { + "@types/validator": "^13.7.10", + "libphonenumber-js": "^1.10.14", + "validator": "^13.7.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -5660,6 +5682,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.10.44", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.44.tgz", + "integrity": "sha512-svlRdNBI5WgBjRC20GrCfbFiclbF0Cx+sCcQob/C1r57nsoq0xg8r65QbTyVyweQIlB33P+Uahyho6EMYgcOyQ==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7874,6 +7901,14 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 2318514..c1bbacc 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^7.1.11", "@prisma/client": "^5.3.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0" diff --git a/prisma/migrations/20230919071959_init/migration.sql b/prisma/migrations/20230919071959_init/migration.sql deleted file mode 100644 index e8c5afa..0000000 --- a/prisma/migrations/20230919071959_init/migration.sql +++ /dev/null @@ -1,16 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "id" SERIAL NOT NULL, - "email" TEXT NOT NULL, - "userCategory" TEXT NOT NULL, - "username" TEXT NOT NULL, - "password" TEXT NOT NULL, - "profilePicture" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92..0000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0564330..58a8293 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,12 +12,82 @@ datasource db { // Dummy user model model User { - id Int @id @default(autoincrement()) - email String @unique - userCategory String + id String @id @unique() @default(uuid()) @db.Uuid + email String @unique + role String username String password String profilePicture String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + requests Request[] + Settlement Settlement[] +} + +// Dummy Admin Model +model Admin { + id String @id @unique() @default(uuid()) @db.Uuid + email String @unique + role String + username String + password String + profilePicture String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Settlement Settlement[] +} + +model Request { + requestId Int @id @default(autoincrement()) + userId String @db.Uuid + title String + type RequestTypeEnum + description String + status RequestStatusEnum + requestContent Json? + responseContent Json? + remark String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id]) // Specify fields and references here + Settlement Settlement? +} + +model Settlement { + settlementId Int @id @default(autoincrement()) + requestId Int @unique + userId String @db.Uuid + adminId String @db.Uuid + requestStatus SettlementStatusEnum + thirdPartyResponseStatus thirdPartyResponseStatusEnum + transactionId Int + content Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id]) // Specify fields and references here + admin Admin @relation(fields: [adminId], references: [id]) // Specify Admin field reference here + request Request @relation(fields: [requestId], references: [requestId]) +} + +enum RequestTypeEnum { + CREDIT + INVOICE_REQUEST + SETTLEMENT +} + +enum RequestStatusEnum { + PENDING + IN_PROGRESS + APPROVED + REJECTED +} + +enum SettlementStatusEnum { + APPROVED + REJECTED +} + +enum thirdPartyResponseStatusEnum { + PENDING + VERIFIED } diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts index d22f389..2b9d42f 100644 --- a/src/app.controller.spec.ts +++ b/src/app.controller.spec.ts @@ -15,8 +15,10 @@ describe('AppController', () => { }); describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); + it('should return "marketplace query service running successfully"', () => { + expect(appController.getHealth()).toBe( + 'marketplace query service running successfully', + ); }); }); }); diff --git a/src/app.controller.ts b/src/app.controller.ts index 9df4fd8..8ef5a2d 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,13 +1,14 @@ import { Controller, Get, HttpStatus } from '@nestjs/common'; import { AppService } from './app.service'; -import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; @Controller() +@ApiTags('get service health') export class AppController { constructor(private readonly appService: AppService) {} @Get() - @ApiOperation({ summary: 'default route for health check' }) // Describes the operation for Swagger. + @ApiOperation({ summary: 'default route for service health check' }) // Describes the operation for Swagger. @ApiResponse({ status: HttpStatus.OK, type: String }) // Describes the response for Swagger. getHealth(): string { return this.appService.getHealth(); diff --git a/src/app.module.ts b/src/app.module.ts index 8468bfa..fc9791c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,6 +3,8 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { PrismaModule } from './prisma/prisma.module'; import { ConfigModule } from '@nestjs/config'; +import { RequestModule } from './request/request.module'; +import { SettlementModule } from './settlement/settlement.module'; @Module({ imports: [ @@ -11,6 +13,8 @@ import { ConfigModule } from '@nestjs/config'; isGlobal: true, // Make the configuration available globally }), PrismaModule, + RequestModule, + SettlementModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/main.ts b/src/main.ts index a32dcf3..c2adf76 100644 --- a/src/main.ts +++ b/src/main.ts @@ -35,7 +35,7 @@ async function bootstrap() { .addApiKey( { type: 'apiKey', - name: 'x-thats-my-college-api-config-key', + name: 'x-marketplace-request-service-api-config-key', in: 'header', }, SWAGGER_CONSTANTS.SWAGGER_AUTH_SECURITY_SCHEMA_API_KEY, // API key security scheme name @@ -52,7 +52,8 @@ async function bootstrap() { .setTitle(SWAGGER_CONSTANTS.TITLE) .setDescription(SWAGGER_CONSTANTS.DESCRIPTION) .setVersion(SWAGGER_CONSTANTS.VERSION) - // .addTag(SWAGGER_TAGS. ) // Add a tag for API grouping + .addTag(SWAGGER_TAGS.REQUEST) // Add a tag for API grouping + .addTag(SWAGGER_TAGS.SETTLEMENT) // Add a tag for API grouping .build(); diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts index 072abe7..2c93095 100644 --- a/src/prisma/prisma.service.ts +++ b/src/prisma/prisma.service.ts @@ -1,4 +1,4 @@ -import { INestApplication, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() diff --git a/src/request/dto/create-request.dto.ts b/src/request/dto/create-request.dto.ts new file mode 100644 index 0000000..fd2857f --- /dev/null +++ b/src/request/dto/create-request.dto.ts @@ -0,0 +1,124 @@ +import { RequestStatusEnum, RequestTypeEnum } from '@prisma/client'; +import { Transform, TransformFnParams } from 'class-transformer'; +import { + IsDate, + IsEnum, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { IsSwaggerEnum } from 'src/utils/decorator/decorators'; + +// CreateRequestDto is used for creating a new request. +export class CreateRequestDto { + // User ID associated with the request. + @IsNotEmpty() + @IsUUID() + userId: string; + + // Title of the request. + @IsNotEmpty() + @IsString() + title: string; + + // Status of the request, should be one of the valid RequestStatusEnum values. + @IsNotEmpty() + @IsEnum(RequestStatusEnum) + @IsSwaggerEnum(RequestStatusEnum) + status: RequestStatusEnum; + + // Description of the request. + @IsNotEmpty() + @IsString() + description: string; + + // Type of the request, should be one of the valid RequestTypeEnum values. + @IsNotEmpty() + @IsEnum(RequestTypeEnum) + @IsSwaggerEnum(RequestTypeEnum) + type: RequestTypeEnum; + + // Optional content of the request in JSON format. + @IsOptional() + // Use the @Transform decorator to apply a custom transformation to a property. + @Transform(({ value }: TransformFnParams) => { + try { + // Try to parse the input value as JSON to convert it into a JavaScript object. + return JSON.parse(value); + } catch (error) { + // If parsing fails (e.g., if value is not a valid JSON string), return the original value. + return value; + } + }) + requestContent?: object; + + // Optional content of the response in JSON format. + @IsOptional() + // Use the @Transform decorator to apply a custom transformation to a property. + @Transform(({ value }: TransformFnParams) => { + try { + // Try to parse the input value as JSON to convert it into a JavaScript object. + return JSON.parse(value); + } catch (error) { + // If parsing fails (e.g., if value is not a valid JSON string), return the original value. + return value; + } + }) + responseContent?: object; + + // Optional remark or additional information about the request. + @IsOptional() + @IsString() + remark?: string; + + // Optional creation date of the request. + @IsDate() + @IsOptional() + createdAt?: Date; + + // Optional update date of the request. + @IsDate() + @IsOptional() + updatedAt?: Date; +} + +// RequestStatusDto is used for updating the status of a request. +export class RequestStatusDto { + // New status for the request, should be one of the valid RequestStatusEnum values. + @IsNotEmpty() + @IsEnum(RequestStatusEnum) + @IsSwaggerEnum(RequestStatusEnum) + status: RequestStatusEnum; +} + +// RequestFilterDto is used for filtering and paginating requests. +export class RequestFilterDto { + // Optional status filter, validate that it's a valid enum value. + @IsOptional() + @IsSwaggerEnum(RequestStatusEnum, { isOptional: true }) + @IsEnum(RequestStatusEnum, { each: true }) + status?: RequestStatusEnum; + + // Optional limit for pagination, validate that it's an integer. + @IsOptional() + @IsInt() + limit?: number; + + // Optional offset for pagination, validate that it's an integer. + @IsOptional() + @IsInt() + offset?: number; + + // Optional field to specify the order of results, validate that it's a string. + @IsOptional() + @IsString() + orderBy?: string; + + // Optional request type filter, validate that it's a valid enum value. + @IsOptional() + @IsSwaggerEnum(RequestTypeEnum, { isOptional: true }) + @IsEnum(RequestTypeEnum, { each: true }) + type?: RequestTypeEnum; +} diff --git a/src/request/dto/response-request.dto.ts b/src/request/dto/response-request.dto.ts new file mode 100644 index 0000000..18dedcc --- /dev/null +++ b/src/request/dto/response-request.dto.ts @@ -0,0 +1,18 @@ +import { RequestStatusEnum, RequestTypeEnum } from '@prisma/client'; +import { IsSwaggerEnum } from 'src/utils/decorator/decorators'; + +export class ResponseRequestDto { + readonly requestId: number; + readonly userId: number; + readonly title: string; + @IsSwaggerEnum(RequestTypeEnum) + readonly type: RequestTypeEnum; + readonly description: string; + @IsSwaggerEnum(RequestStatusEnum) + readonly status: RequestStatusEnum; + readonly requestContent: object; + readonly responseContent: object; + readonly remark: string; + readonly createdAt: Date; + readonly updatedAt: Date; +} diff --git a/src/request/dto/update-request.dto.ts b/src/request/dto/update-request.dto.ts new file mode 100644 index 0000000..c50456b --- /dev/null +++ b/src/request/dto/update-request.dto.ts @@ -0,0 +1,84 @@ +import { + IsDate, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, +} from 'class-validator'; +import { RequestStatusEnum, RequestTypeEnum } from '@prisma/client'; +import { Transform, TransformFnParams } from 'class-transformer'; +import { IsSwaggerEnum } from 'src/utils/decorator/decorators'; + +export class UpdateRequestDto { + // Title of the request. + @IsOptional() + @IsNotEmpty() + @IsString() + title: string; + + // Status of the request, should be one of the valid RequestStatusEnum values. + @IsOptional() + @IsNotEmpty() + @IsEnum(RequestStatusEnum) + @IsSwaggerEnum(RequestStatusEnum) + status: RequestStatusEnum; + + // Description of the request. + @IsOptional() + @IsString() + @IsNotEmpty() + description: string; + + // Type of the request, should be one of the valid RequestTypeEnum values. + @IsNotEmpty() + @IsOptional() + @IsEnum(RequestTypeEnum) + @IsSwaggerEnum(RequestTypeEnum) + type: RequestTypeEnum; + + // Optional content of the request in JSON format. + @IsNotEmpty() + @IsOptional() + // Use the @Transform decorator to apply a custom transformation to a property. + @Transform(({ value }: TransformFnParams) => { + try { + // Try to parse the input value as JSON to convert it into a JavaScript object. + return JSON.parse(value); + } catch (error) { + // If parsing fails (e.g., if value is not a valid JSON string), return the original value. + return value; + } + }) + requestContent?: object; + + // Optional content of the response in JSON format. + @IsNotEmpty() + @IsOptional() + // Use the @Transform decorator to apply a custom transformation to a property. + @Transform(({ value }: TransformFnParams) => { + try { + // Try to parse the input value as JSON to convert it into a JavaScript object. + return JSON.parse(value); + } catch (error) { + // If parsing fails (e.g., if value is not a valid JSON string), return the original value. + return value; + } + }) + responseContent?: object; + + // Optional remark or additional information about the request. + @IsOptional() + @IsNotEmpty() + @IsString() + remark?: string; + + // Optional creation date of the request. + @IsDate() + @IsOptional() + createdAt?: Date; + + // Optional update date of the request. + @IsDate() + @IsOptional() + updatedAt?: Date; +} diff --git a/src/request/entities/request.entity.ts b/src/request/entities/request.entity.ts new file mode 100644 index 0000000..d5e892a --- /dev/null +++ b/src/request/entities/request.entity.ts @@ -0,0 +1 @@ +export class Request {} diff --git a/src/request/enum/request.enum.ts b/src/request/enum/request.enum.ts new file mode 100644 index 0000000..25e5a40 --- /dev/null +++ b/src/request/enum/request.enum.ts @@ -0,0 +1,12 @@ +export enum RequestTypeEnum { + CREDIT, + INVOICE_REQUEST, + SETTLEMENT, +} + +export enum RequestStatusEnum { + PENDING, + IN_PROGRESS, + APPROVED, + REJECTED, +} diff --git a/src/request/request.controller.ts b/src/request/request.controller.ts new file mode 100644 index 0000000..4bc8efe --- /dev/null +++ b/src/request/request.controller.ts @@ -0,0 +1,256 @@ +import { + Controller, + Post, + Get, + Body, + Param, + Patch, + Query, + HttpStatus, + Logger, + Res, +} from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { RequestService } from './request.service'; +import { + CreateRequestDto, + RequestFilterDto, + RequestStatusDto, +} from './dto/create-request.dto'; +import { UpdateRequestDto } from './dto/update-request.dto'; +import { ResponseRequestDto } from './dto/response-request.dto'; + +@Controller('requests') +@ApiTags('requests') +export class RequestController { + // Create a logger instance for this controller to log events and errors. + private readonly logger = new Logger(RequestController.name); + + // Constructor for the RequestController class, injecting the RequestService. + constructor(private readonly requestService: RequestService) {} + + @Post() + @ApiOperation({ summary: 'Create a request' }) // Describes the api operation for Swagger. + @ApiResponse({ status: HttpStatus.CREATED, type: ResponseRequestDto }) // Describes the response for Swagger. + async createRequest(@Res() res, @Body() createRequestDto: CreateRequestDto) { + try { + // Log the initiation of request creation + this.logger.log( + `Creating a new request for user id #${createRequestDto.userId}`, + ); + + const request = await this.requestService.createRequest(createRequestDto); + + // Log the successful creation of the request + this.logger.log( + `Successfully created a new request for user id #${request.userId}, request id ${request.requestId}`, + ); + + // Return a success response with the created request data + return res + .status(HttpStatus.CREATED) + .json({ message: 'Request created successfully', data: request }); + } catch (error) { + // Log the error if request creation fails + this.logger.error( + `Failed to create new request for user id #${createRequestDto.userId}`, + error, + ); + + // Return an error response + return res.status(HttpStatus.CREATED).json({ + message: `Failed to create the request for user id #${createRequestDto.userId}`, + }); + } + } + + @Get() + @ApiOperation({ summary: 'Get all requests' }) + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseRequestDto, + isArray: true, + }) + async getAllRequests(@Res() res, @Query() filter: RequestFilterDto) { + try { + this.logger.log(`Initiated fetching requests`); + + const requests = await this.requestService.getAllRequests(filter); + + this.logger.log(`Successfully fetched requests`); + + return res + .status(HttpStatus.OK) + .json({ message: 'Successfully fetched requests', data: requests }); + } catch (error) { + this.logger.error(`Failed to fetch requests`, error); + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: 'Failed to fetch requests' }); + } + } + + @Get('user/:userId') + @ApiOperation({ summary: 'Get all requests of a user' }) + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseRequestDto, + isArray: true, + }) + async getAllRequestsForUser( + @Res() res, + @Param('userId') userId: string, + @Query() filter: RequestFilterDto, + ) { + try { + this.logger.log( + `Initiated fetching user requests for user id #${userId}`, + ); + + const requests = await this.requestService.getAllRequestsForUser( + userId, + filter, + ); + + this.logger.log( + `Successfully fetched user requests for user id #${userId}`, + ); + + return res.status(HttpStatus.OK).json({ + message: `Successfully fetched user requests for user id #${userId}`, + data: requests, + }); + } catch (error) { + this.logger.error( + `Failed to fetch user requests for user id #${userId}`, + error, + ); + + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + message: `Failed to fetch user requests for user id #${userId}`, + }); + } + } + + @Get(':requestId') + @ApiOperation({ summary: 'Get a request' }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseRequestDto }) + async getRequestById(@Res() res, @Param('requestId') requestId: number) { + try { + this.logger.log( + `Initiated fetching request for request id #${requestId}`, + ); + + const request = await this.requestService.getRequestById(+requestId); + + this.logger.log( + `Successfully fetched request for request id #${requestId}`, + ); + + return res.status(HttpStatus.OK).json({ + message: `Successfully fetched request for request id #${requestId}`, + data: request, + }); + } catch (error) { + this.logger.error( + `Failed to fetch request for request id #${requestId}`, + error, + ); + + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + message: + error.message || + `Failed to fetch request for request id #${requestId}`, + }); + } + } + + @Patch('update/:requestId') + @ApiOperation({ summary: 'Update a request' }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseRequestDto }) + async updateRequestByRequestId( + @Res() res, + @Param('requestId') requestId: number, + @Body() updateRequestDto: UpdateRequestDto, + ) { + try { + this.logger.log( + `Initiated updating request for request id #${requestId}`, + ); + + const updatedRequest = await this.requestService.updateRequestByRequestId( + +requestId, + updateRequestDto, + ); + + this.logger.log( + `Successfully updated request for request id #${requestId}`, + ); + + return res.status(HttpStatus.OK).json({ + message: `Successfully updated request for request id #${requestId}`, + data: updatedRequest, + }); + } catch (error) { + console.log('error:', error); + this.logger.error( + `Failed to update request for request id #${requestId}`, + error, + ); + + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + message: + error.meta.cause || + `Failed to updated request for request id #${requestId}`, + }); + } + } + + @Patch('update/status/:requestId') + @ApiOperation({ summary: 'Update a request status' }) + @ApiResponse({ status: HttpStatus.OK, type: ResponseRequestDto }) + async updateRequestStatus( + @Res() res, + @Param('requestId') requestId: number, + @Body() updateRequestStatusDto: RequestStatusDto, + ) { + try { + this.logger.log( + `Initiated updating request status to ${JSON.stringify( + updateRequestStatusDto, + )} for request id #${requestId}`, + ); + + const updatedRequest = await this.requestService.updateRequestStatus( + +requestId, + updateRequestStatusDto, + ); + + this.logger.log( + `Successfully updated request status to ${JSON.stringify( + updateRequestStatusDto, + )} for request id #${requestId}`, + ); + + return res.status(HttpStatus.OK).json({ + message: `Successfully updated request status for request id #${requestId}`, + data: updatedRequest, + }); + } catch (error) { + this.logger.error( + `Failed to update request status to ${JSON.stringify( + updateRequestStatusDto, + )} for request id #${requestId}`, + error, + ); + + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + message: + error.meta.cause || + `Failed to updated request status to ${JSON.stringify( + updateRequestStatusDto, + )} for request id #${requestId}`, + }); + } + } +} diff --git a/src/request/request.module.ts b/src/request/request.module.ts new file mode 100644 index 0000000..91c477b --- /dev/null +++ b/src/request/request.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { RequestController } from './request.controller'; +import { RequestService } from './request.service'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { PrismaModule } from 'src/prisma/prisma.module'; + +@Module({ + // Import the PrismaModule to make Prisma service available within this module + imports: [PrismaModule], + + // Declare the RequestController as a controller for this module + controllers: [RequestController], + + // Declare the RequestService and PrismaService as providers for this module + providers: [RequestService, PrismaService], + + // Export the RequestService to make it available for other modules that import this module + exports: [RequestService], +}) +export class RequestModule {} diff --git a/src/request/request.service.ts b/src/request/request.service.ts new file mode 100644 index 0000000..6c5adc9 --- /dev/null +++ b/src/request/request.service.ts @@ -0,0 +1,92 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; // Import your Prisma service +import { + CreateRequestDto, + RequestFilterDto, + RequestStatusDto, +} from './dto/create-request.dto'; +import { UpdateRequestDto } from './dto/update-request.dto'; + +@Injectable() +export class RequestService { + constructor(private prisma: PrismaService) {} + + // Create a new request + async createRequest(createRequestDto: CreateRequestDto) { + return this.prisma.request.create({ + data: createRequestDto, + }); + } + + // Get all requests with optional filters and pagination + async getAllRequests(filter: RequestFilterDto) { + const { status, type, limit = 10, offset = 0, orderBy } = filter; + return this.prisma.request.findMany({ + where: { + status: status ?? undefined, // Optional status filter + type: type ?? undefined, // Optional type filter + }, + orderBy: { + [orderBy || 'createdAt']: 'asc', // Default sorting by createdAt + }, + skip: +offset, + take: +limit, + }); + } + + // Get all requests for a specific user with optional filters and pagination + async getAllRequestsForUser(userId: string, filter: RequestFilterDto) { + const { status, type, limit = 10, offset = 0, orderBy } = filter; + return this.prisma.request.findMany({ + where: { + userId: userId, + status: status ?? undefined, // Optional status filter + type: type ?? undefined, // Optional type filter + }, + orderBy: { + [orderBy || 'createdAt']: 'asc', // Default sorting by createdAt + }, + skip: offset, + take: limit, + }); + } + + // Get a request by its ID + async getRequestById(requestId: number) { + const request = await this.prisma.request.findUnique({ + where: { + requestId: requestId, + }, + }); + if (!request) { + throw new NotFoundException(`Request not found with id #${requestId}`); + } + return request; + } + + // Update a request by its ID + async updateRequestByRequestId( + requestId: number, + updateRequestDto: UpdateRequestDto, + ) { + return this.prisma.request.update({ + where: { + requestId: requestId, + }, + data: updateRequestDto, + }); + } + + // Update the status of a request by its ID + async updateRequestStatus( + requestId: number, + updateRequestStatusDto: RequestStatusDto, + ) { + return this.prisma.request.update({ + where: { + requestId: requestId, + }, + data: updateRequestStatusDto, + }); + } +} diff --git a/src/settlement/dto/create-settlement.dto.ts b/src/settlement/dto/create-settlement.dto.ts new file mode 100644 index 0000000..1728e9e --- /dev/null +++ b/src/settlement/dto/create-settlement.dto.ts @@ -0,0 +1,104 @@ +import { + SettlementStatusEnum, + thirdPartyResponseStatusEnum, +} from '@prisma/client'; +import { Transform, TransformFnParams } from 'class-transformer'; +import { + IsDate, + IsEnum, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { IsSwaggerEnum } from 'src/utils/decorator/decorators'; + +// createSettlementDto is used for creating the new settlement request +export class CreateSettlementDto { + // Request ID associated with the request + @IsNotEmpty() + @IsInt() + requestId: number; + + // User ID associated with the request. + @IsNotEmpty() + @IsUUID() + userId: string; + + // Admin Id associated with the Admin + @IsNotEmpty() + @IsUUID() + adminId: string; + + // Status of the request, should be one of the valid SettlementStatusEnum values. + @IsNotEmpty() + @IsEnum(SettlementStatusEnum) + @IsSwaggerEnum(SettlementStatusEnum) + requestStatus: SettlementStatusEnum; + + // Status of the request, should be one of the valid UserResponseStatusEnum values. + @IsNotEmpty() + @IsEnum(thirdPartyResponseStatusEnum) + @IsSwaggerEnum(thirdPartyResponseStatusEnum) + thirdPartyResponseStatus: thirdPartyResponseStatusEnum; + + // Mandatory field transaction ID for the settlement request. + @IsNotEmpty() + @IsInt() + transactionId: number; + + // Optional content of the request in JSON format. + @IsOptional() + // Use the @Transform decorator to apply a custom transformation to a property. + @Transform(({ value }: TransformFnParams) => { + try { + // Try to parse the input value as JSON to convert it into a JavaScript object. + return JSON.parse(value); + } catch (error) { + // If parsing fails (e.g., if value is not a valid JSON string), return the original value. + return value; + } + }) + content?: object; + + // Optional creation date of the request. + @IsDate() + @IsOptional() + createdAt?: Date = new Date(); + + // Optional update date of the request. + @IsDate() + @IsOptional() + updatedAt?: Date; +} + +// SettlementFilterDto is used for filtering and paginating requests. +export class SettlementFilterDto { + // Optional settlement status filter, validated that it's valid SettlementStatusEnum value + @IsOptional() + @IsSwaggerEnum(SettlementStatusEnum, { isOptional: true }) + @IsEnum(SettlementStatusEnum, { each: true }) + requestStatus?: SettlementStatusEnum; + + // Optional third party status filter, validate that it's valid thirdPartyResponseStatusEnum value + @IsOptional() + @IsSwaggerEnum(thirdPartyResponseStatusEnum, { isOptional: true }) + @IsEnum(thirdPartyResponseStatusEnum, { each: true }) + thirdPartyResponseStatus?: thirdPartyResponseStatusEnum; + + // Optional limit for pagination, validate that it's an integer. + @IsOptional() + @IsInt() + limit?: number; + + // Optional offset for pagination, validate that it's an integer. + @IsOptional() + @IsInt() + offset?: number; + + // Optional field to specify the order of results, validate that it's a string. + @IsOptional() + @IsString() + orderBy?: string; +} diff --git a/src/settlement/dto/response-settlement.dto.ts b/src/settlement/dto/response-settlement.dto.ts new file mode 100644 index 0000000..3b2f144 --- /dev/null +++ b/src/settlement/dto/response-settlement.dto.ts @@ -0,0 +1,19 @@ +import { + SettlementStatusEnum, + thirdPartyResponseStatusEnum, +} from '@prisma/client'; +import { IsSwaggerEnum } from 'src/utils/decorator/decorators'; + +export class ResponseSettlementDto { + readonly SettlementId: number; + readonly requestId: number; + readonly userId: number; + readonly adminId: number; + @IsSwaggerEnum(thirdPartyResponseStatusEnum) + readonly type: thirdPartyResponseStatusEnum; + readonly transactionId: number; + @IsSwaggerEnum(SettlementStatusEnum) + readonly requestStatus: SettlementStatusEnum; + readonly createdAt: Date; + readonly updatedAt: Date; +} diff --git a/src/settlement/dto/update-settlement.dto.ts b/src/settlement/dto/update-settlement.dto.ts new file mode 100644 index 0000000..2da5013 --- /dev/null +++ b/src/settlement/dto/update-settlement.dto.ts @@ -0,0 +1,45 @@ +import { + SettlementStatusEnum, + thirdPartyResponseStatusEnum, +} from '@prisma/client'; +import { Transform, TransformFnParams } from 'class-transformer'; +import { IsDate, IsEnum, IsNotEmpty, IsOptional } from 'class-validator'; +import { IsSwaggerEnum } from 'src/utils/decorator/decorators'; + +export class UpdateSettlementDto { + // Status of the request, should be one of the valid SettlementStatusEnum values. + @IsNotEmpty() + @IsEnum(SettlementStatusEnum) + @IsSwaggerEnum(SettlementStatusEnum) + requestStatus: SettlementStatusEnum; + + // Status of the request, should be one of the valid UserResponseStatusEnum values. + @IsNotEmpty() + @IsEnum(thirdPartyResponseStatusEnum) + @IsSwaggerEnum(thirdPartyResponseStatusEnum) + thirdPartyResponseStatus: thirdPartyResponseStatusEnum; + + // Optional content of the request in JSON format. + @IsOptional() + // Use the @Transform decorator to apply a custom transformation to a property. + @Transform(({ value }: TransformFnParams) => { + try { + // Try to parse the input value as JSON to convert it into a JavaScript object. + return JSON.parse(value); + } catch (error) { + // If parsing fails (e.g., if value is not a valid JSON string), return the original value. + return value; + } + }) + content?: object; + + // Optional creation date of the request. + @IsDate() + @IsOptional() + createdAt?: Date; + + // Optional update date of the request. + @IsDate() + @IsOptional() + updatedAt?: Date; +} diff --git a/src/settlement/enum/settlement.enum.ts b/src/settlement/enum/settlement.enum.ts new file mode 100644 index 0000000..eb4bb64 --- /dev/null +++ b/src/settlement/enum/settlement.enum.ts @@ -0,0 +1,9 @@ +export enum SettlementStatusEnum { + APPROVED, + REJECTED, +} + +export enum thirdPartyResponseStatusEnum { + PENDING, + VERIFIED, +} diff --git a/src/settlement/settlement.controller.spec.ts b/src/settlement/settlement.controller.spec.ts new file mode 100644 index 0000000..93dc158 --- /dev/null +++ b/src/settlement/settlement.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SettlementController } from './settlement.controller'; +import { SettlementService } from './settlement.service'; + +describe('SettlementController', () => { + let controller: SettlementController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [SettlementController], + providers: [SettlementService], + }).compile(); + + controller = module.get(SettlementController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/settlement/settlement.controller.ts b/src/settlement/settlement.controller.ts new file mode 100644 index 0000000..6cbaf9b --- /dev/null +++ b/src/settlement/settlement.controller.ts @@ -0,0 +1,223 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Query, + Logger, + HttpStatus, + Res, +} from '@nestjs/common'; +import { SettlementService } from './settlement.service'; +import { + CreateSettlementDto, + SettlementFilterDto, +} from './dto/create-settlement.dto'; +import { UpdateSettlementDto } from './dto/update-settlement.dto'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ResponseSettlementDto } from './dto/response-settlement.dto'; + +@Controller('settlement') +@ApiTags('settlement') +export class SettlementController { + // Create a logger instance for this controller to log events and errors. + private readonly logger = new Logger(SettlementController.name); + + // Constructor for the SettlementController class, injecting the settlement service + constructor(private settlementService: SettlementService) {} + + // API to create a new settlement request. + @Post() + @ApiOperation({ summary: 'Create a request' }) // Describes the api operation for Swagger. + @ApiResponse({ status: HttpStatus.CREATED, type: ResponseSettlementDto }) // Describes the response for Swagger. + async createSettlement( + @Res() res, + @Body() createSettlementDto: CreateSettlementDto, + ) { + try { + // Log the initiation of settlement creation + this.logger.log( + `Creating a new settlement for user id #${createSettlementDto.userId}`, + ); + + const settlement = await this.settlementService.createSettlement( + createSettlementDto, + ); + + // Log the successful creation of the settlement + this.logger.log( + `Successfully created a new settlement for user id #${settlement.userId}, request id ${settlement.requestId}`, + ); + + // Return a success response with the created request data + return res.status(HttpStatus.CREATED).json({ + message: 'Settlement request created successfully', + data: settlement, + }); + } catch (error) { + // Log the error if request creation fails + this.logger.error( + `Failed to create new request for user id #${createSettlementDto.userId}`, + error, + ); + + // Return an error response + return res.status(HttpStatus.CREATED).json({ + message: `Failed to create the request for user id #${createSettlementDto.userId}`, + }); + } + } + + // API to get all the settlements + @Get() + @ApiOperation({ summary: 'Get all settlements' }) // Api oepration for swagger. + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseSettlementDto, + isArray: true, + }) // Describe response for swagger + async getAllSettlements(@Res() res, @Query() filter: SettlementFilterDto) { + try { + this.logger.log(`Initiated fetching Settlements`); + + const settlements = await this.settlementService.getAllSettlements( + filter, + ); + + this.logger.log(`Successfully fetched settlements`); + + return res.status(HttpStatus.OK).json({ + message: 'Successfully fetched settlements', + data: settlements, + }); + } catch (error) { + this.logger.error(`Failed to fetch settlements`, error); + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ message: 'Failed to fetch settlements' }); + } + } + + // API to get all the settlements by the userId. + @Get('user/:userId') + @ApiOperation({ summary: 'Get all the settlement of a user.' }) + @ApiResponse({ + status: HttpStatus.OK, + type: ResponseSettlementDto, + isArray: true, + }) // Describes the response for Swagger. + async getUserSettlementsById( + @Param('userId') userId: string, + @Res() res, + @Query() filter: SettlementFilterDto, + ) { + try { + this.logger.log( + `Initiated fetching user settlements for user id #${userId}`, + ); + + const settlements = await this.settlementService.getAllSettlementForUser( + userId, + filter, + ); + + this.logger.log( + `Successfully fetched user settlements for user id #${userId}`, + ); + + return res.status(HttpStatus.OK).json({ + message: `Successfully fetched user settlements for user id #${userId}`, + data: settlements, + }); + } catch (error) { + this.logger.error( + `Failed to fetch user settlements for user id #${userId}`, + error, + ); + + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + message: + error.message || `Failed to fetch request for request id #${userId}`, + }); + } + } + + // Get settlement by request ID + @Get(':requestId') + @ApiOperation({ summary: 'Get a settlement by request ID' }) // Describes the api operation for Swagger. + @ApiResponse({ status: HttpStatus.OK, type: ResponseSettlementDto }) // Describes the response for Swagger. + async getSettlementById(@Res() res, @Param('requestId') requestId: number) { + try { + this.logger.log( + `Initiated fetching settlement for request id #${requestId}`, + ); + + const settlement = await this.settlementService.getsettlementById( + +requestId, + ); + + this.logger.log( + `Successfully fetched settlement for request id #${requestId}`, + ); + + return res.status(HttpStatus.OK).json({ + message: `Successfully fetched settlement for request id #${requestId}`, + data: settlement, + }); + } catch (error) { + this.logger.error( + `Failed to fetch settlement for request id #${requestId}`, + error, + ); + + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + message: + error.message || + `Failed to fetch request for request id #${requestId}`, + }); + } + } + + // update the settlement by the requestID + @Patch(':requestId') + @ApiOperation({ summary: 'Update a settlement request' }) // Describes the api operation for Swagger. + @ApiResponse({ status: HttpStatus.OK, type: ResponseSettlementDto }) // Describes the response for Swagger. + async updateSettlementByRequestId( + @Res() res, + @Param('requestId') requestId: number, + @Body() updateSettlementDto: UpdateSettlementDto, + ) { + try { + this.logger.log( + `Initiated updating settlement request for request id #${requestId}`, + ); + + const updatedSettlement = + await this.settlementService.updateSettlementByRequestId( + +requestId, + updateSettlementDto, + ); + + this.logger.log( + `Successfully updated settlement request for request id #${requestId}`, + ); + return res.status(HttpStatus.OK).json({ + message: `Successfully updated settlement request for request id #${requestId}`, + data: updatedSettlement, + }); + } catch (error) { + this.logger.error( + `Failed to update settlement request for request id #${requestId}`, + error, + ); + + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + message: + error.message || + `Failed to fetch request for request id #${requestId}`, + }); + } + } +} diff --git a/src/settlement/settlement.module.ts b/src/settlement/settlement.module.ts new file mode 100644 index 0000000..c161d23 --- /dev/null +++ b/src/settlement/settlement.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { SettlementService } from './settlement.service'; +import { SettlementController } from './settlement.controller'; +import { PrismaModule } from '../prisma/prisma.module'; +import { PrismaService } from '../prisma/prisma.service'; + +@Module({ + // Import the PrismaModule to make Prisma service available within this module + imports: [PrismaModule], + + // Declare the SettlemetController as a controller for this module + controllers: [SettlementController], + + // Declare the SettlementService and PrismaService as providers for this module + providers: [SettlementService, PrismaService], + + // Export the RequestService to make it available for other modules that import this module + exports: [SettlementService], +}) +export class SettlementModule {} diff --git a/src/settlement/settlement.service.spec.ts b/src/settlement/settlement.service.spec.ts new file mode 100644 index 0000000..ed6e549 --- /dev/null +++ b/src/settlement/settlement.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SettlementService } from './settlement.service'; + +describe('SettlementService', () => { + let service: SettlementService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SettlementService], + }).compile(); + + service = module.get(SettlementService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/settlement/settlement.service.ts b/src/settlement/settlement.service.ts new file mode 100644 index 0000000..af17126 --- /dev/null +++ b/src/settlement/settlement.service.ts @@ -0,0 +1,149 @@ +import { + Injectable, + NotAcceptableException, + NotFoundException, +} from '@nestjs/common'; +import { + CreateSettlementDto, + SettlementFilterDto, +} from './dto/create-settlement.dto'; +import { PrismaService } from '../prisma/prisma.service'; +import { UpdateSettlementDto } from './dto/update-settlement.dto'; + +@Injectable() +export class SettlementService { + constructor(private prisma: PrismaService) {} + async createSettlement(createSettlementDto: CreateSettlementDto) { + // Check for duplicate requestId + const existingSettlement = await this.checkForDuplicateRequest( + createSettlementDto, + ); + + if (existingSettlement) { + throw new NotAcceptableException( + 'Duplicate requestId. Settlement already exists.', + ); + } + + // If no duplicate, create the new settlement + const newSettlement = await this.prisma.settlement.create({ + data: createSettlementDto, + }); + return newSettlement; + } + + // to check the dublicate entry of the existing requestId + async checkForDuplicateRequest(createSettlementDto: CreateSettlementDto) { + const existingSettlement = await this.prisma.settlement.findFirst({ + where: { + requestId: createSettlementDto.requestId, + }, + }); + return existingSettlement; + } + + // get all settlements with optional filter and pagination + async getAllSettlements(filter: SettlementFilterDto) { + const { + thirdPartyResponseStatus, + requestStatus, + limit = 10, + offset = 0, + orderBy, + } = filter; + return this.prisma.settlement.findMany({ + where: { + requestStatus: requestStatus ?? undefined, // Optional status filter + thirdPartyResponseStatus: thirdPartyResponseStatus ?? undefined, // Optional response filter + }, + orderBy: { + [orderBy || 'createdAt']: 'asc', // Default sorting by createdAt + }, + skip: offset, + take: limit, + }); + } + + // Get settlement for a specific user using the userId + async getAllSettlementForUser(userId: string, filter: SettlementFilterDto) { + // Check if the user exists + const user = await this.prisma.user.findUnique({ + where: { + id: userId, + }, + }); + + if (!user) { + throw new NotFoundException(`User with ID ${userId} not found.`); + } + + // get the settlement if the userId exist. + const { + thirdPartyResponseStatus, + requestStatus, + limit = 10, + offset = 0, + orderBy, + } = filter; + return this.prisma.settlement.findMany({ + where: { + userId: userId, + requestStatus: requestStatus ?? undefined, // Optional status filter + thirdPartyResponseStatus: thirdPartyResponseStatus ?? undefined, // Optional thirdPartyResponseStatus filter + }, + orderBy: { + [orderBy || 'createdAt']: 'asc', // Default sorting by createdAt + }, + skip: offset, + take: limit, + }); + } + + // Get a settlement by its request ID + async getsettlementById(requestId: number) { + // Check the the settlement is available for the given requestId. + const findRequestId = await this.prisma.request.findUnique({ + where: { + requestId: requestId, + }, + }); + + if (!findRequestId) { + // Handle the case where the requestId does not exist in the database + throw new NotFoundException(`Given requestId ${requestId} not found.`); + } + + // Get settlement by the requestId + return this.prisma.settlement.findMany({ + where: { + requestId: requestId, + }, + }); + } + + // Update the requested settlement status by its ID + async updateSettlementByRequestId( + requestId: number, + updateSettlementStatusDto: UpdateSettlementDto, + ) { + // Check the the requestId is available for settlement + const findRequestId = await this.prisma.request.findUnique({ + where: { + requestId: requestId, + }, + }); + + if (!findRequestId) { + // Handle the case where the requestId does not exist in the database + throw new NotFoundException(`Given requestId ${requestId} not found.`); + } + + // Update the settlement by the request Id + return this.prisma.settlement.updateMany({ + where: { + requestId: requestId, + }, + data: updateSettlementStatusDto, + }); + } +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 2881a49..6794332 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -4,11 +4,14 @@ export const CONSTANTS = { export const SWAGGER_CONSTANTS = { TITLE: 'API', - DESCRIPTION: 'Marketplace Query Service API description', + DESCRIPTION: 'Marketplace Request Service API description', VERSION: '1.0', - PATH: 'api/doc', + PATH: 'api/docs', SWAGGER_AUTH_SECURITY_SCHEMA_API_KEY: 'api-config-key', SWAGGER_AUTH_SECURITY_SCHEMA_JWT: 'jwt', }; -export enum SWAGGER_TAGS {} +export enum SWAGGER_TAGS { + REQUEST = 'requests', + SETTLEMENT = 'settlement', +} diff --git a/src/utils/decorator/decorators.ts b/src/utils/decorator/decorators.ts new file mode 100644 index 0000000..4772838 --- /dev/null +++ b/src/utils/decorator/decorators.ts @@ -0,0 +1,39 @@ +import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger'; + +// Decorator to document enum properties in Swagger +// @param enumObject: The enum object to document +// @param options: An object with options for the decorator (including isOptional) +export function IsSwaggerEnum( + enumObject: Record, + options: { + isOptional?: boolean; + apiPropertyOptions?: ApiPropertyOptions; // Additional decorator options for ApiProperty + } = {}, +) { + return (target: any, propertyKey: string) => { + const { isOptional = false, apiPropertyOptions = {} } = options; // Destructure options + + // Get the enum values from the provided enumObject + const values = Object.values(enumObject); + + // Create decorator options for ApiProperty + const decoratorOptions = { + type: enumObject, // Use the cloned enumObject + enum: values, // Specify the enum values + ...apiPropertyOptions, // Merge additional ApiProperty options + }; + + // If the property is not optional, add the example property + if (!isOptional) { + decoratorOptions.example = values.join(' || '); // Set an example enum value for documentation purposes + } + + // If the property is optional, mark it as such in Swagger documentation + if (isOptional) { + decoratorOptions['required'] = false; + } + + // Apply the ApiProperty decorator with the specified options to the property + ApiProperty(decoratorOptions)(target, propertyKey); + }; +}