-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature:FUR-49 [BE][Web] Review & Rating product (#119)
- Loading branch information
Showing
10 changed files
with
325 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters