Skip to content

Commit

Permalink
feature:FUR-49 [BE][Web] Review & Rating product (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
MinhhTien authored Jun 15, 2024
1 parent 60f9526 commit bc444b5
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 2 deletions.
8 changes: 7 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { TaskModule } from '@task/task.module'
import { AnalyticModule } from '@analytic/analytic.module'
import { PaymentModule } from '@payment/payment.module'
import { AIGenerationModule } from '@ai-generation/ai-generation.module'
import { ReviewModule } from '@review/review.module'

@Module({
imports: [
Expand Down Expand Up @@ -111,6 +112,10 @@ import { AIGenerationModule } from '@ai-generation/ai-generation.module'
{
path: 'ai-generation',
module: AIGenerationModule
},
{
path: 'reviews',
module: ReviewModule
}
]),
CommonModule,
Expand All @@ -127,7 +132,8 @@ import { AIGenerationModule } from '@ai-generation/ai-generation.module'
TaskModule,
AnalyticModule,
PaymentModule,
AIGenerationModule
AIGenerationModule,
ReviewModule
],
controllers: [AppController],
providers: [AppService]
Expand Down
6 changes: 6 additions & 0 deletions src/common/contracts/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,9 @@ export enum AnalyticPeriod {
MONTH = 'MONTH',
YEAR = 'YEAR',
}

export enum ReviewStatus {
ACTIVE = 'ACTIVE',
INACTIVE = 'INACTIVE',
DELETED = 'DELETED'
}
7 changes: 6 additions & 1 deletion src/common/contracts/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,10 @@ export const Errors: Record<string, ErrorResponse> = {
error: 'NOT_ENOUGH_CREDITS_ERROR',
message: 'Số lượng credits còn lại không đủ. Vui lòng nạp thêm nhé.',
httpStatus: HttpStatus.BAD_REQUEST
}
},
REVIEW_ALREADY_EXIST: {
error: 'REVIEW_ALREADY_EXIST',
message: 'Bạn đã review sản phẩm này.',
httpStatus: HttpStatus.BAD_REQUEST
},
}
50 changes: 50 additions & 0 deletions src/review/controllers/customer.review.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common'
import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'
import * as _ from 'lodash'
import { Roles } from '@auth/decorators/roles.decorator'
import { ReviewStatus, UserRole } from '@common/contracts/constant'
import { RolesGuard } from '@auth/guards/roles.guard'
import { JwtAuthGuard } from '@auth/guards/jwt-auth.guard'
import { ReviewService } from '@review/services/review.service'
import { PaginationQuery, SuccessDataResponse } from '@common/contracts/dto'
import { Pagination, PaginationParams } from '@common/decorators/pagination.decorator'
import { CreateReviewDto, FilterReviewDto, ReviewResponseDto } from '@review/dtos/review.dto'

@ApiTags('Review - Customer')
@ApiBearerAuth()
@Roles(UserRole.CUSTOMER)
@UseGuards(JwtAuthGuard.ACCESS_TOKEN, RolesGuard)
@Controller('customer')
export class ReviewController {
constructor(private readonly reviewService: ReviewService) {}

@ApiOperation({
summary: 'Customer can review product after order completed'
})
@ApiOkResponse({ type: SuccessDataResponse })
@Post()
createReview(@Req() req, @Body() createReviewDto: CreateReviewDto) {
createReviewDto.customerId = _.get(req, 'user._id')
return this.reviewService.createReview(createReviewDto)
}

@ApiOperation({
summary: 'Paginate product review list'
})
@ApiOkResponse({ type: ReviewResponseDto })
@ApiQuery({ type: PaginationQuery })
@Get()
async paginate(@Pagination() paginationParams: PaginationParams, @Query() filterReviewDto: FilterReviewDto) {
const condition = {
product: filterReviewDto.productId,
status: {
$ne: ReviewStatus.DELETED
}
}

if (filterReviewDto.rate) {
condition['rate'] = filterReviewDto.rate
}
return await this.reviewService.getReviewList(condition, paginationParams)
}
}
60 changes: 60 additions & 0 deletions src/review/dtos/review.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { DataResponse } from '@common/contracts/openapi-builder'
import { IsInt, IsMongoId, IsNotEmpty, IsOptional, Max, MaxLength, Min } from 'class-validator'
import { Customer } from '@customer/schemas/customer.schema'
import { Type } from 'class-transformer'

export class CreateReviewDto {
@ApiProperty()
@IsNotEmpty()
@IsMongoId()
productId: string

@ApiProperty()
@IsNotEmpty()
@Max(5)
@Min(1)
@IsInt()
rate: number

@ApiProperty()
@IsNotEmpty()
@MaxLength(1024)
comment: string

customerId?: string
}

export class FilterReviewDto {
@ApiProperty()
@IsNotEmpty()
@IsMongoId()
productId: string

@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@Max(5)
@Min(1)
@IsInt()
rate: number
}

export class ReviewDto {
@ApiProperty()
_id: string

@ApiProperty({ type: Customer })
customer: Customer;

@ApiProperty()
product: string;

@ApiProperty()
rate: number

@ApiProperty()
comment: string
}

export class ReviewResponseDto extends DataResponse(ReviewDto) {}
12 changes: 12 additions & 0 deletions src/review/repositories/review.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PaginateModel } from 'mongoose'
import { Injectable } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { AbstractRepository } from '@common/repositories'
import { Review, ReviewDocument } from '@review/schemas/review.schema'

@Injectable()
export class ReviewRepository extends AbstractRepository<ReviewDocument> {
constructor(@InjectModel(Review.name) model: PaginateModel<ReviewDocument>) {
super(model)
}
}
23 changes: 23 additions & 0 deletions src/review/review.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Global, Module } from '@nestjs/common'
import { MongooseModule } from '@nestjs/mongoose'
import { HttpModule } from '@nestjs/axios'
import { Review, ReviewSchema } from './schemas/review.schema'
import { CustomerModule } from '@customer/customer.module'
import { ReviewController } from './controllers/customer.review.controller'
import { ReviewService } from './services/review.service'
import { ReviewRepository } from './repositories/review.repository'
import { ProductModule } from '@product/product.module'

@Global()
@Module({
imports: [
MongooseModule.forFeature([{ name: Review.name, schema: ReviewSchema }]),
HttpModule,
CustomerModule,
ProductModule
],
controllers: [ReviewController],
providers: [ReviewService, ReviewRepository],
exports: [ReviewService, ReviewRepository]
})
export class ReviewModule {}
54 changes: 54 additions & 0 deletions src/review/schemas/review.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { HydratedDocument, Types } from 'mongoose'
import * as paginate from 'mongoose-paginate-v2'
import { Transform } from 'class-transformer'
import { ApiProperty } from '@nestjs/swagger'
import { Customer } from '@customer/schemas/customer.schema'
import { Product } from '@product/schemas/product.schema'
import { ReviewStatus } from '@common/contracts/constant'

export type ReviewDocument = HydratedDocument<Review>

@Schema({
collection: 'reviews',
timestamps: true,
toJSON: {
transform(doc, ret) {
delete ret.__v
}
}
})
export class Review {
constructor(id?: string) {
this._id = id
}
@ApiProperty()
@Transform(({ value }) => value?.toString())
_id: string

@ApiProperty({ type: Customer })
@Prop({ type: Types.ObjectId, ref: Customer.name })
customer: Customer;

@ApiProperty({ type: Product })
@Prop({ type: Types.ObjectId, ref: Product.name })
product: Product;

@ApiProperty()
@Prop({ type: Number })
rate: number

@ApiProperty()
@Prop({ type: String })
comment: string

@Prop({
enum: ReviewStatus,
default: ReviewStatus.ACTIVE
})
status: ReviewStatus
}

export const ReviewSchema = SchemaFactory.createForClass(Review)

ReviewSchema.plugin(paginate)
106 changes: 106 additions & 0 deletions src/review/services/review.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Injectable, Logger } from '@nestjs/common'
import { ReviewRepository } from '@review/repositories/review.repository'
import { FilterQuery } from 'mongoose'
import { Review } from '@review/schemas/review.schema'
import { PaginationParams } from '@common/decorators/pagination.decorator'
import { CreateReviewDto } from '@review/dtos/review.dto'
import { ProductRepository } from '@product/repositories/product.repository'
import { ProductStatus } from '@common/contracts/constant'
import { AppException } from '@common/exceptions/app.exception'
import { Errors } from '@common/contracts/error'
import { SuccessResponse } from '@common/contracts/dto'

@Injectable()
export class ReviewService {
private readonly logger = new Logger(ReviewService.name)
constructor(
private readonly reviewRepository: ReviewRepository,
private readonly productRepository: ProductRepository
) {}

public async getReviewList(filter: FilterQuery<Review>, paginationParams: PaginationParams) {
const result = await this.reviewRepository.paginate(
{
...filter
},
{
...paginationParams,
populate: [
{
path: 'customer',
select: {
_id: 1,
firstName: 1,
lastName: 1,
email: 1,
avatar: 1
}
}
],
projection: {
_id: 1,
customer: 1,
rate: 1,
comment: 1,
createdAt: 1,
updatedAt: 1
}
}
)
return result
}

public async createReview(createReviewDto: CreateReviewDto) {
// 1. Check if customer has completed order product

// 2. Save review
// 2.1 Check valid product
const product = await this.productRepository.findOne({
conditions: {
_id: createReviewDto.productId,
status: {
$ne: ProductStatus.DELETED
}
}
})
if (!product) throw new AppException(Errors.PRODUCT_NOT_FOUND)

// 2.2 Check already review
const review = await this.reviewRepository.findOne({
conditions: {
customer: createReviewDto.customerId,
product: createReviewDto.productId
}
})
if (review) throw new AppException(Errors.REVIEW_ALREADY_EXIST)

await this.reviewRepository.create({
...createReviewDto,
customer: createReviewDto.customerId,
product: createReviewDto.productId
})

// 3. Update rating product
const avgRate = await this.reviewRepository.model.aggregate([
{
$group: {
_id: '$product',
avgRating: { $avg: '$rate' }
}
},
{ $project: { roundedAvgRating: { $round: ['$avgRating', 1] } } }
])

this.logger.debug('ReviewService.createReview: ', avgRate[0]?.roundedAvgRating)
if (!!avgRate[0]?.roundedAvgRating) {
await this.productRepository.findOneAndUpdate(
{ _id: createReviewDto.productId },
{
rate: avgRate[0]?.roundedAvgRating
}
)
}

return new SuccessResponse(true)
}
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@analytic/*": ["src/analytic/*"],
"@payment/*": ["src/payment/*"],
"@ai-generation/*": ["src/ai-generation/*"],
"@review/*": ["src/review/*"],
"@src/*": ["src/*"]
},

Expand Down

0 comments on commit bc444b5

Please sign in to comment.