From 8b47be99ca3bb1c3d56b44a7b09bdea349432c6f Mon Sep 17 00:00:00 2001 From: IRADUKUNDA SANGWA CEDRIC <110623461+Dawaic6@users.noreply.github.com> Date: Tue, 28 May 2024 08:45:08 +0200 Subject: [PATCH] update profile (#72) review controller adding testing fix lint issue --- src/__test__/review.test.ts | 94 ++++++++++++++++++++++++++++ src/config/db.ts | 4 +- src/controller/productController.ts | 9 ++- src/controller/reviewController.ts | 91 +++++++++++++++++++++++++++ src/database/models/productEntity.ts | 8 +++ src/database/models/reviewEntity.ts | 22 +++++++ src/docs/reviewDocs.ts | 66 +++++++++++++++++++ src/routes/index.ts | 3 + src/routes/reviewRoutes.ts | 11 ++++ 9 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 src/__test__/review.test.ts create mode 100644 src/controller/reviewController.ts create mode 100644 src/database/models/reviewEntity.ts create mode 100644 src/docs/reviewDocs.ts create mode 100644 src/routes/reviewRoutes.ts diff --git a/src/__test__/review.test.ts b/src/__test__/review.test.ts new file mode 100644 index 00000000..d038ecc7 --- /dev/null +++ b/src/__test__/review.test.ts @@ -0,0 +1,94 @@ +import request from 'supertest'; +import app from '../app'; +import { + afterAllHook, + beforeAllHook, + getBuyerToken, + getVendorToken, +} from './testSetup'; +beforeAll(beforeAllHook); +afterAll(afterAllHook); + +describe('Review controller test', () => { + let buyerToken: string; + let vendorToken: string; + let productId: number; + let categoryId: number; + + beforeAll(async () => { + buyerToken = await getBuyerToken(); + vendorToken = await getVendorToken(); + }); + + it('should return review product has successfully', async () => { + const categoryData = { + name: 'Category4', + description: 'category description', + }; + + const categoryResponse = await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${vendorToken}`) + .send(categoryData); + + categoryId = categoryResponse.body.data.id; + + const productData = { + name: 'New Product Two', + image: 'new_product.jpg', + gallery: [], + shortDesc: 'This is a new product', + longDesc: 'Detailed description of the new product', + categoryId: categoryId, + quantity: 10, + regularPrice: 5, + salesPrice: 4, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + }; + + const responseProduct = await request(app) + .post('/api/v1/product') + .set('Authorization', `Bearer ${vendorToken}`) + .send(productData); + + + productId = responseProduct.body.data.id; + const reviewBody = {content:'good', rating:5, productId} + const responseReview = await request(app) + .post('/api/v1/review') + .set('Authorization', `Bearer ${buyerToken}`) + .send(reviewBody); + expect(responseReview.statusCode).toEqual(201); + expect(responseReview.body.message).toEqual('Review created successfully'); + expect(responseReview.body.review).toBeDefined(); + }), + + it('should return 404 if product is not found', async () => { + const reviewBody = {content:'good', rating:5, productId:99} + const responseReview = await request(app) + .post('/api/v1/review') + .set('Authorization', `Bearer ${buyerToken}`) + .send(reviewBody); + expect(responseReview.statusCode).toEqual(404); + expect(responseReview.body.message).toEqual('Product not found'); + }), + it('should return 200 Ok to get all reviews ', async () => { + const responseReview = await request(app) + .get('/api/v1/review') + .set('Authorization', `Bearer ${buyerToken}`) + expect(responseReview.statusCode).toEqual(200); + expect(responseReview.body.reviews).toBeDefined(); + }), + it('should return 409 if the review exist', async () => { + const reviewBody = {content:'good', rating:5, productId} + const responseReview = await request(app) + .post('/api/v1/review') + .set('Authorization', `Bearer ${buyerToken}`) + .send(reviewBody) + expect(responseReview.statusCode).toEqual(409); + expect(responseReview.body.message).toEqual('you are already reviewed the product'); + + }) +}) \ No newline at end of file diff --git a/src/config/db.ts b/src/config/db.ts index dafc83ab..6082fa09 100644 --- a/src/config/db.ts +++ b/src/config/db.ts @@ -28,14 +28,14 @@ const staging = { password: process.env.DB_PASSWORD, host: process.env.DB_HOST, port: process.env.DB_PORT, - name: process.env.DB_NAME, + name: process.env.DB_NAME_DEV, }; const production = { username: process.env.DB_USER, password: process.env.DB_PASSWORD, host: process.env.DB_HOST, port: process.env.DB_PORT, - name: process.env.DB_NAME, + name: process.env.DB_NAME_DEV, }; const config: { diff --git a/src/controller/productController.ts b/src/controller/productController.ts index eb13774b..6049c1e3 100644 --- a/src/controller/productController.ts +++ b/src/controller/productController.ts @@ -259,8 +259,15 @@ export const getProduct = errorHandler(async (req: Request, res: Response) => { vendor: { firstName: true, }, + reviews:{ + content:true, + rating:true, + user:{ + firstName:true + } + }, }, - relations: ['category', 'vendor'], + relations: ['category', 'vendor','reviews'], }); if (!product) { diff --git a/src/controller/reviewController.ts b/src/controller/reviewController.ts new file mode 100644 index 00000000..069b6c7a --- /dev/null +++ b/src/controller/reviewController.ts @@ -0,0 +1,91 @@ +import { Request, Response } from 'express'; +import { Review } from '../database/models/reviewEntity'; +import Product from '../database/models/productEntity'; +import dbConnection from '../database'; +import errorHandler from '../middlewares/errorHandler'; +import UserModel from '../database/models/userModel'; + +const productRepository = dbConnection.getRepository(Product); +const reviewRepository = dbConnection.getRepository(Review); + const userRepository = dbConnection.getRepository(UserModel); + +export const createReview = errorHandler(async (req: Request, res: Response) => { + const { content, rating, productId } = req.body; + const userId = req.user!.id; + + const product = await productRepository.findOne({ + where:{ + id:productId + } + }); + if (!product) { + return res.status(404).json({ message: 'Product not found' }); + } + + const user = await userRepository.findOne({ + where: { + id: userId, + }, + select: { + id: true, + }, + }); + + const existingReview = await reviewRepository.findOne({ + where:{ + user:{ + id:userId + }, + product:{ + id:productId + } + } + }) + + if(existingReview){ + return res.status(409).json({ message: 'you are already reviewed the product' }); + } + + const newReview = new Review(); + newReview.content= content; + newReview.rating=parseInt(rating) ; + newReview.user = user!; + newReview.product = product; + + const review = await reviewRepository.save(newReview); + + const reviews = await reviewRepository.find({ + where:{ + product:{ + id:productId + } + }, + relations:['user','product'] + }); + let totalRating = 0; + for (const review of reviews) { + totalRating += review.rating; + } + product.averageRating = Number((totalRating / reviews.length).toPrecision(2)); + + + await productRepository.save(product); + + return res.status(201).json({ message: 'Review created successfully', review }); +}); + + +export const getReviews = errorHandler(async (req: Request, res: Response) => { + const reviews = await reviewRepository.find({ + select:{ + user:{ + firstName:true + }, + product:{ + name:true + } + }, + relations:['user','product'] + }) + return res.status(200).json({ reviews }); +}); \ No newline at end of file diff --git a/src/database/models/productEntity.ts b/src/database/models/productEntity.ts index e036eea8..05dee853 100644 --- a/src/database/models/productEntity.ts +++ b/src/database/models/productEntity.ts @@ -5,9 +5,11 @@ import { CreateDateColumn, UpdateDateColumn, ManyToOne, + OneToMany, } from 'typeorm'; import Category from './categoryEntity'; import UserModel from './userModel'; +import { Review } from './reviewEntity'; @Entity() export default class Product { @@ -49,6 +51,12 @@ export default class Product { @Column({ default: true }) isAvailable: boolean; + + @Column('float',{ default:0}) + averageRating: number; + + @OneToMany(() => Review, review => review.product) + reviews: Review[]; @ManyToOne(() => UserModel, { onDelete: 'CASCADE' }) vendor: UserModel; diff --git a/src/database/models/reviewEntity.ts b/src/database/models/reviewEntity.ts new file mode 100644 index 00000000..7c1b8180 --- /dev/null +++ b/src/database/models/reviewEntity.ts @@ -0,0 +1,22 @@ + +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import Product from './productEntity'; +import User from './userModel'; + +@Entity() +export class Review { + @PrimaryGeneratedColumn() + id: number; + + @Column() + content: string; + + @Column() + rating: number; + + @ManyToOne(() => User, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + user: User; + + @ManyToOne(() => Product, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + product: Product; +} diff --git a/src/docs/reviewDocs.ts b/src/docs/reviewDocs.ts new file mode 100644 index 00000000..e06e2299 --- /dev/null +++ b/src/docs/reviewDocs.ts @@ -0,0 +1,66 @@ +/** + * @swagger + * /api/v1/review: + * post: + * summary: Create a new review + * tags: [Reviews] + * consumes: + * - application/json + * produces: + * - application/json + * parameters: + * - in: body + * name: review + * description: The review object + * required: true + * schema: + * type: object + * properties: + * content: + * type: string + * description: The content of the review + * rating: + * type: integer + * description: The rating of the review (1 to 5) + * productId: + * type: integer + * description: The ID of the product being reviewed + * example: + * content: "This product is amazing!" + * rating: 5 + * productId: 50 + * responses: + * '201': + * description: Review created successfully + * schema: + * type: object + * properties: + * message: + * type: string + * description: A success message + * review: + * '400': + * description: Bad request, check the request body + * '404': + * description: Product not found + * '409': + * description: User has already reviewed the product + */ + +/** + * @swagger + * /api/v1/review: + * get: + * summary: Get reviews by user + * tags: [Reviews] + * security: + * - bearerAuth: [] + * responses: + * '200': + * description: Reviews retrieved successfully + * schema: + * type: object + * properties: + * reviews: + * type: array + */ diff --git a/src/routes/index.ts b/src/routes/index.ts index bdf772b4..b2fb9092 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -7,6 +7,8 @@ import buyerRoutes from './buyerRoutes'; import cartRoutes from '../routes/cartRoutes'; import couponRouter from './couponRoute'; import chekoutRoutes from './checkoutRoutes'; +import reviewRoute from './reviewRoutes'; + const router = Router(); router.use('/user', userRouter); @@ -17,5 +19,6 @@ router.use('/buyer', buyerRoutes); router.use('/cart', cartRoutes); router.use('/coupons', couponRouter); router.use('/checkout', chekoutRoutes); +router.use('/review',reviewRoute) export default router; diff --git a/src/routes/reviewRoutes.ts b/src/routes/reviewRoutes.ts new file mode 100644 index 00000000..871000a1 --- /dev/null +++ b/src/routes/reviewRoutes.ts @@ -0,0 +1,11 @@ + +import {createReview ,getReviews} from '../controller/reviewController'; +import { Router } from 'express'; +import { IsLoggedIn } from '../middlewares/isLoggedIn'; +import { checkRole } from '../middlewares/authorize'; + +const reviewRoute = Router(); +reviewRoute.use(IsLoggedIn , checkRole(['Buyer'])) +reviewRoute.route('/').post( createReview).get(getReviews) + +export default reviewRoute;