diff --git a/docs/rating-controller.md b/docs/rating-controller.md new file mode 100644 index 0000000..041ee86 --- /dev/null +++ b/docs/rating-controller.md @@ -0,0 +1,144 @@ +# User controller + +--- + +[UserRatingAverageCount](#userRatingAverageCount) + +## get user rating average&count (update authorized user) + +| Key | Description | +| ------ | ------------- | +| Method | `GET` | +| Path | `/rating/averageCount/:userId` | + +### Success Response + +HTTP Code: 200 + +```json +{ + "status": true, + "statusCode": 200, + "data": { + "_avg": { + "ratingOf10": 6 + }, + "_count": { + "ratingOf10": 4 + } + } +} +``` + +### Invalid credential + +HTTP Code: 401 + +```json +{ + "status": true, + "statusCode": 401, + "data": { + "access": "UNAUTHORIZED" + } +} +``` + +### user not found + +HTTP Code: 200 + +```json +{ + "status": true, + "statusCode": 200, + "data": { + "_avg": { + "ratingOf10": null + }, + "_count": { + "ratingOf10": 0 + } + } +} + +``` + +--- + +# User controller + +--- + +[CreateUserRating](#createUserRating) + +## get user rating average&count (update authorized user) + +| Key | Description | +| ------ | ------------- | +| Method | `POST` | +| Path | `/rating/craete` | + +### Request Body + +| Key | Type | +| ---------- | ------ | +| `userId` | String | +| `recruiterUserId` | String | +| `ratingOf10` | number | +| `contractId` | String? | + +### Success Response + +HTTP Code: 200 + +```json +{ + "status": true, + "statusCode": 200, + "data": { + "id": "de67c8bd-8637-4aa0-9983-31c602896672", + "userId": "5c03f41e-5c18-4ef2-adf2-9ef560c34ca0", + "recruiterUserId": "741c71e9-94dc-4ad8-98a6-9259c6b72f88", + "ratingOf10": 9, + "createdAt": "2024-01-15T16:16:41.196Z", + "updatedAt": "2024-01-15T16:16:41.196Z" + } +} +``` + +### Invalid credential + +HTTP Code: 401 + +```json +{ + "status": true, + "statusCode": 401, + "data": { + "access": "UNAUTHORIZED" + } +} +``` + +### invalid field + +HTTP Code: 400 + +```json +{ + "status": false, + "statusCode": 400, + "message": { + "name": "PrismaClientKnownRequestError", + "code": "P2003", + "clientVersion": "5.7.1", + "meta": { + "modelName": "Rating", + "field_name": "Rating_userId_fkey (index)" + } + }, + "error": "BAD_REQUEST" +} + +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c1bff0d..4086435 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "backend-8tech", - "version": "4.0.3", + "version": "4.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "backend-8tech", - "version": "4.0.3", + "version": "4.0.4", "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^9.4.3", diff --git a/package.json b/package.json index 60fd935..9bc4432 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "backend-8tech", - "version": "4.0.3", + "version": "4.0.4", "description": "", "author": "8tech", "private": true, diff --git a/prisma/migrations/20240115160307_updated_contract/migration.sql b/prisma/migrations/20240115160307_updated_contract/migration.sql new file mode 100644 index 0000000..17650f7 --- /dev/null +++ b/prisma/migrations/20240115160307_updated_contract/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "Contract" ADD COLUMN "ratingId" TEXT, +ADD COLUMN "workSubmission" TEXT; + +-- AddForeignKey +ALTER TABLE "Contract" ADD CONSTRAINT "Contract_ratingId_fkey" FOREIGN KEY ("ratingId") REFERENCES "Rating"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4f308d2..3901a51 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,10 +7,10 @@ generator client { } datasource db { - provider = "postgresql" - url = env("DATABASE_URL") + provider = "postgresql" + url = env("DATABASE_URL") // #DEBUG : comment the directUrl for development in windows or running in local - directUrl = env("DIRECT_URL") + // directUrl = env("DIRECT_URL") } model User { @@ -60,21 +60,24 @@ model Company { } model Contract { - id String @id @default(uuid()) - userId String - jobId String - paymentId String? - title String - description String - paymentRate Int - template String? @db.VarChar() - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - status ContractStatus @default(PENDING) - customField String? - JobVacancy JobVacancy? @relation(fields: [jobId], references: [id]) - User User? @relation(fields: [userId], references: [id]) - Payment Payment? @relation(fields: [paymentId], references: [id]) + id String @id @default(uuid()) + userId String + jobId String + paymentId String? + title String + description String + paymentRate Int + template String? @db.VarChar() + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + status ContractStatus @default(PENDING) + customField String? + workSubmission String? + ratingId String? + JobVacancy JobVacancy? @relation(fields: [jobId], references: [id]) + User User? @relation(fields: [userId], references: [id]) + Payment Payment? @relation(fields: [paymentId], references: [id]) + Rating Rating? @relation(fields: [ratingId], references: [id]) } model Payment { @@ -88,13 +91,14 @@ model Payment { } model Rating { - id String @id @default(uuid()) - User User @relation(fields: [userId], references: [id]) + id String @id @default(uuid()) + User User @relation(fields: [userId], references: [id]) userId String recruiterUserId String ratingOf10 Int - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Contract Contract[] } enum Role { diff --git a/src/contract/contract.controller.ts b/src/contract/contract.controller.ts index 4886679..4e7470b 100644 --- a/src/contract/contract.controller.ts +++ b/src/contract/contract.controller.ts @@ -25,6 +25,7 @@ import { IContractPayoutLink, } from './interface/contract.interface'; import { Contract } from '@prisma/client'; +import { ContractUpdateDto } from './dto/contract-update.dto'; @ApiTags('Contract') @Controller('contract') @@ -41,6 +42,17 @@ export class ContractController { return response; } + @ApiBearerAuth() + @Roles(Role.USER) + @UseGuards(JwtAuthGuard, RoleGuard) + @Post('update') + async updateContract(@Res() res, @Body() data: ContractUpdateDto) { + console.info('#ContractUpdate request incoming with: ', data); + const { id, ...contract } = data; + const response = await this.contractService.update(id, contract); + return response; + } + @Get('generate/:contractId') @ApiParam({ name: 'contractId', type: String }) @Roles(Role.USER) diff --git a/src/contract/contract.repository.ts b/src/contract/contract.repository.ts index 2fbf86c..4003867 100644 --- a/src/contract/contract.repository.ts +++ b/src/contract/contract.repository.ts @@ -16,6 +16,15 @@ export class ContractRepository { }); } + async update(id: string, contract: any): Promise { + return this.prisma.contract.update({ + data: contract, + where: { + id: contract.id, + }, + }); + } + async getAllbyUserId(userId: string): Promise { return this.prisma.contract.findMany({ where: { diff --git a/src/contract/contract.service.ts b/src/contract/contract.service.ts index 830aacc..a5b864e 100644 --- a/src/contract/contract.service.ts +++ b/src/contract/contract.service.ts @@ -33,8 +33,12 @@ export class ContractService { //#region Contract //Create Contract - async create(user: any): Promise { - return this.contractRepository.create(user); + async create(data: any): Promise { + return this.contractRepository.create(data); + } + + async update(id: string, data: any): Promise { + return this.contractRepository.update(id, data); } async generate(contractId) { diff --git a/src/contract/dto/contract-update.dto.ts b/src/contract/dto/contract-update.dto.ts new file mode 100644 index 0000000..35c9446 --- /dev/null +++ b/src/contract/dto/contract-update.dto.ts @@ -0,0 +1,51 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; + +export class ContractUpdateDto { + @ApiProperty() + @IsString() + @IsNotEmpty({ + message: 'MUST_NOT_BE_EMPTY', + }) + readonly id: string; + + @ApiProperty() + @IsString() + @IsOptional() + readonly userId: string; + + @ApiProperty() + @IsString() + @IsOptional() + readonly jobId: string; + + @ApiProperty() + @IsString() + @IsOptional() + readonly title: string; + + @ApiProperty() + @IsString() + @IsOptional() + readonly description: string; + + @ApiProperty() + @IsNumber() + @IsOptional() + readonly paymentRate: number; + + @ApiProperty() + @IsString() + @IsOptional() + readonly paymentRequestId?: string; + + @ApiProperty() + @IsString() + @IsOptional() + readonly template?: string; + + @ApiProperty() + @IsString() + @IsOptional() + readonly customField?: string; +} diff --git a/src/contract/test/contract.controller.spec.ts b/src/contract/test/contract.controller.spec.ts index ff656d5..6355be9 100644 --- a/src/contract/test/contract.controller.spec.ts +++ b/src/contract/test/contract.controller.spec.ts @@ -61,6 +61,8 @@ describe('ContractController', () => { createdAt: new Date(), updatedAt: new Date(), customField: null, + workSubmission: null, + ratingId: null, }; const createSpy = jest diff --git a/src/job/job.repository.ts b/src/job/job.repository.ts index c44dce5..7f6fbee 100644 --- a/src/job/job.repository.ts +++ b/src/job/job.repository.ts @@ -39,6 +39,11 @@ export class JobRepository { }, }; }, + companyId(keyword: string) { + return { + companyId: keyword, + }; + }, }; } diff --git a/src/rating/dto/rating-create.dto.ts b/src/rating/dto/rating-create.dto.ts index fd2cc77..74042a2 100644 --- a/src/rating/dto/rating-create.dto.ts +++ b/src/rating/dto/rating-create.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; +import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; export class RatingCreateDto { @ApiProperty() @@ -16,4 +16,9 @@ export class RatingCreateDto { @IsNotEmpty() @IsNumber() readonly ratingOf10: number; + + @ApiProperty() + @IsOptional() + @IsString() + readonly contractId?: string; } diff --git a/src/rating/rating.controller.ts b/src/rating/rating.controller.ts index 5c951a7..544b243 100644 --- a/src/rating/rating.controller.ts +++ b/src/rating/rating.controller.ts @@ -1,5 +1,13 @@ -import { Body, Controller, Post, Res, UseGuards } from '@nestjs/common'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { + Body, + Controller, + Get, + Param, + Post, + Res, + UseGuards, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiParam, ApiTags } from '@nestjs/swagger'; import { RatingService } from './rating.service'; import { RatingCreateDto } from './dto/rating-create.dto'; import { Roles } from '../auth/roles/role.decorator'; @@ -30,4 +38,16 @@ export class RatingController { const response = await this.ratingService.update(data); return response; } + + @ApiBearerAuth() + @Roles(Role.USER) + @UseGuards(JwtAuthGuard, RoleGuard) + @Get('averageCount/:userId') + @ApiParam({ name: 'userId', type: String }) + async averageCount(@Res() res, @Param() params: any) { + const response = await this.ratingService.userRatingAverageCount( + params.userId, + ); + return response; + } } diff --git a/src/rating/rating.module.ts b/src/rating/rating.module.ts index f363946..5694a87 100644 --- a/src/rating/rating.module.ts +++ b/src/rating/rating.module.ts @@ -3,9 +3,15 @@ import { RatingService } from './rating.service'; import { PrismaService } from '../prisma/prisma.service'; import { RatingController } from './rating.controller'; import { RatingRepository } from './rating.repository'; +import { ContractRepository } from '../contract/contract.repository'; @Module({ - providers: [RatingService, PrismaService, RatingRepository], + providers: [ + RatingService, + PrismaService, + RatingRepository, + ContractRepository, + ], exports: [RatingService], controllers: [RatingController], }) diff --git a/src/rating/rating.repository.ts b/src/rating/rating.repository.ts index 2cd9e8a..587dbbb 100644 --- a/src/rating/rating.repository.ts +++ b/src/rating/rating.repository.ts @@ -1,6 +1,5 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; -import { RatingCreateDto } from './dto/rating-create.dto'; @Injectable() export class RatingRepository { @@ -10,10 +9,14 @@ export class RatingRepository { this.model = this.prisma.rating; } - async create(rating: RatingCreateDto): Promise { - return this.prisma.rating.create({ - data: rating, - }); + async create(rating: any): Promise { + return this.prisma.rating + .create({ + data: rating, + }) + .catch((err) => { + throw new HttpException(err, HttpStatus.BAD_REQUEST); + }); } async update(ratingId: string, data: any): Promise { @@ -24,4 +27,18 @@ export class RatingRepository { data, }); } + + async averageCount(userId: string): Promise { + return this.prisma.rating.aggregate({ + _avg: { + ratingOf10: true, + }, + _count: { + ratingOf10: true, + }, + where: { + userId, + }, + }); + } } diff --git a/src/rating/rating.service.ts b/src/rating/rating.service.ts index 041f46a..567b4a3 100644 --- a/src/rating/rating.service.ts +++ b/src/rating/rating.service.ts @@ -1,19 +1,34 @@ import { Injectable } from '@nestjs/common'; import { RatingRepository } from './rating.repository'; -import { IRating } from './interface/rating.interface'; import { RatingCreateDto } from './dto/rating-create.dto'; import { RatingUpdateDto } from './dto/rating-update.dto'; +import { ContractRepository } from '../contract/contract.repository'; +import { Rating } from '@prisma/client'; @Injectable() export class RatingService { - constructor(private ratingRepository: RatingRepository) {} + constructor( + private ratingRepository: RatingRepository, + private contractRepository: ContractRepository, + ) {} - async create(rating: RatingCreateDto): Promise { - return this.ratingRepository.create(rating); + async create(rating: RatingCreateDto): Promise { + const { contractId, ...data } = rating; + const res = await this.ratingRepository.create(data); + if (contractId) { + this.contractRepository.update(contractId, { + ratingId: res.id, + }); + } + return res; } - async update(rating: RatingUpdateDto): Promise { + async update(rating: RatingUpdateDto): Promise { const { id, ...data } = rating; return this.ratingRepository.update(id, data); } + + async userRatingAverageCount(userId: string): Promise { + return this.ratingRepository.averageCount(userId); + } } diff --git a/src/rating/test/rating.controller.spec.ts b/src/rating/test/rating.controller.spec.ts index a5371c0..59907f0 100644 --- a/src/rating/test/rating.controller.spec.ts +++ b/src/rating/test/rating.controller.spec.ts @@ -8,6 +8,7 @@ import { RatingService } from '../rating.service'; import { IRating } from '../interface/rating.interface'; import { RatingCreateDto } from '../dto/rating-create.dto'; import { RatingUpdateDto } from '../dto/rating-update.dto'; +import { Rating } from '@prisma/client'; describe('RatingController', () => { let controller: RatingController; @@ -45,10 +46,13 @@ describe('RatingController', () => { ratingOf10: 9, }; - const mockJob: IRating = { + const mockJob: Rating = { id: 'ratingId', recruiterUserId: 'userId', ratingOf10: 9, + createdAt: new Date(), + updatedAt: new Date(), + userId: 'userId', }; const createSpy = jest @@ -104,10 +108,13 @@ describe('RatingController', () => { recruiterUserId: 'test', ratingOf10: 9, }; - const mockJob: IRating = { + const mockJob: Rating = { id: 'ratingId', recruiterUserId: 'userId', ratingOf10: 9, + createdAt: new Date(), + updatedAt: new Date(), + userId: 'userId', }; const updateSpy = jest diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 71ef050..0936ccc 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -44,7 +44,7 @@ export class UsersService { const existingUser = await this.userRepository.findOnebyUsername( data.username, ); - if (existingUser !== null) { + if (existingUser !== null && existingUser.id !== id) { throw new HttpException( { username: 'ALREADY_USED',