From 43373e0355778854daa71290836c1eda732778ab Mon Sep 17 00:00:00 2001 From: wayneleon1 Date: Fri, 19 Jul 2024 17:29:04 +0200 Subject: [PATCH] fix: the Admin should get more details about coupons --- src/__test__/coupon.test.ts | 659 ++++++++++++++-------------- src/controller/couponController.ts | 322 ++++++++------ src/database/models/couponEntity.ts | 87 ++-- src/docs/couponDocs.ts | 6 +- src/routes/couponRoute.ts | 25 +- 5 files changed, 590 insertions(+), 509 deletions(-) diff --git a/src/__test__/coupon.test.ts b/src/__test__/coupon.test.ts index 50514977..e9d12a4c 100644 --- a/src/__test__/coupon.test.ts +++ b/src/__test__/coupon.test.ts @@ -2,332 +2,341 @@ import request from 'supertest'; import app from '../app'; import { getVendorToken, afterAllHook, beforeAllHook } from './testSetup'; - beforeAll(beforeAllHook); afterAll(afterAllHook); - describe('Coupon Controller Tests', () => { - let token: string; - let couponId: number; - let productId: number; - - beforeAll(async () => { - token = await getVendorToken(); - }); - - it('should create a new coupon with valid data', async () => { - const categoryData = { - name: 'Category', - description: 'category description', - icon: 'category icon' - }; - - const categoryResponse = await request(app) - .post('/api/v1/category') - .set('Authorization', `Bearer ${token}`) - .send(categoryData); - - const categoryId = categoryResponse.body.data.id; - const productData = { - name: 'New Product', - 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 productResponse = await request(app) - .post('/api/v1/product') - .set('Authorization', `Bearer ${token}`) - .send(productData); - - expect(productResponse.statusCode).toEqual(201); - expect(productResponse.body.message).toEqual('Product successfully created'); - expect(productResponse.body.data).toBeDefined(); - productId = productResponse.body.data.id; - - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .post('/api/v1/coupons') - .set('Authorization', `Bearer ${token}`) - .send(couponData); - - expect(response.statusCode).toEqual(201); - expect(response.body.message).toEqual('Coupon created successfully'); - expect(response.body.data).toBeDefined(); - couponId = response.body.data.id; - }); - - it('should return validation errors for invalid coupon data', async () => { - const invalidCouponData = { - description: '', - percentage: 120, - expirationDate: '2022-12-31', - applicableProducts: [], - }; - - const response = await request(app) - .post('/api/v1/coupons') - .set('Authorization', `Bearer ${token}`) - .send(invalidCouponData); - - expect(response.statusCode).toEqual(400); - expect(response.body.errors).toBeDefined(); - }); - - it ('should return a 404 for a non-existent product', async () => { - const nonExistentProductId = 999; - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [nonExistentProductId], - }; - - const response = await request(app) - .post('/api/v1/coupons') - .set('Authorization', `Bearer ${token}`) - .send(couponData); - - expect(response.statusCode).toEqual(400); - expect(response.body.error).toEqual(`Product with id ${nonExistentProductId} not found`); - }) - - it('should return a 404 for a non-existent coupon', async () => { - const nonExistentCouponId = 999; - const response = await request(app).get(`/api/v1/coupons/${nonExistentCouponId}`); - - expect(response.statusCode).toEqual(404); - expect(response.body.error).toEqual('Coupon not found'); - }); - - it('should return a 401 for an unauthenticated user', async () => { - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .post('/api/v1/coupons') - .set('Authorization', 'Invalid Token') - .send(couponData); - expect(response.statusCode).toEqual(401); - }) - - it('should return a 403 for a user trying to create a coupon for another user\'s product', async () => { - const otherVendorToken = await getVendorToken( - 'email@example.com', - 'Password123', - 'OtherVendor', - 'OtherName' - ) - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .post('/api/v1/coupons') - .set('Authorization', `Bearer ${otherVendorToken}`) - .send(couponData); - expect(response.statusCode).toEqual(403); - expect(response.body.error).toEqual('You can only create coupons for your own products'); - }) - - it('should retrieve all coupons', async () => { - const response = await request(app).get('/api/v1/coupons'); - - expect(response.statusCode).toEqual(200); - expect(Array.isArray(response.body)).toBeTruthy(); - }); - - it('should retrieve all coupons by vendor', async () => { - const response = await request(app) - .get('/api/v1/coupons/mine') - .set('Authorization', `Bearer ${token}`); - - expect(response.statusCode).toEqual(200); - expect(Array.isArray(response.body)).toBeTruthy(); - }) - - it('should retrieve a single coupon by ID', async () => { - const response = await request(app).get(`/api/v1/coupons/${couponId}`); - - expect(response.statusCode).toEqual(200); - expect(response.body).toBeDefined(); - }); - - it('should update a coupon by ID', async () => { - const updatedCouponData = { - description: 'Updated Coupon', - percentage: 20, - expirationDate: '2023-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .put(`/api/v1/coupons/${couponId}`) - .set('Authorization', `Bearer ${token}`) - .send(updatedCouponData); - - expect(response.statusCode).toEqual(200); - expect(response.body).toBeDefined(); - }); - - it('should return a 404 for a non-existent coupon while updating', async () => { - const nonExistentCouponId = 999; - const updatedCouponData = { - description: 'Updated Coupon', - percentage: 20, - expirationDate: '2023-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .put(`/api/v1/coupons/${nonExistentCouponId}`) - .set('Authorization', `Bearer ${token}`) - .send(updatedCouponData); - - expect(response.statusCode).toEqual(404); - expect(response.body.error).toEqual('Coupon not found'); - }); - - - it('should return a 403 for a user trying to update a coupon for another user', async () => { - const otherVendorToken = await getVendorToken( - 'email@example.com', - 'Password123', - 'OtherVendor', - 'OtherName' - ) - const couponData = { - description: 'Updated Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .put(`/api/v1/coupons/${couponId}`) - .set('Authorization', `Bearer ${otherVendorToken}`) - .send(couponData); - expect(response.statusCode).toEqual(403); - expect(response.body.error).toEqual('You can only create coupons for your own products'); - }) - - it('should return a 401 for an unauthenticated user', async () => { - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .put(`/api/v1/coupons/${couponId}`) - .set('Authorization', 'Invalid Token') - .send(couponData); - expect(response.statusCode).toEqual(401); - }) - - it('should return a 401 for an unauthenticated user', async () => { - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .post('/api/v1/coupons') - .set('Authorization', 'Invalid Token') - .send(couponData); - expect(response.statusCode).toEqual(401); - }) - - it ('should return a 404 for a non-existent product', async () => { - const nonExistentProductId = 999; - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [nonExistentProductId], - }; - - const response = await request(app) - .put(`/api/v1/coupons/${couponId}`) - .set('Authorization', `Bearer ${token}`) - .send(couponData); - - expect(response.statusCode).toEqual(400); - expect(response.body.error).toEqual(`Product with id ${nonExistentProductId} not found`); - }) - - it('should return validation errors for invalid update data', async () => { - const invalidUpdateData = { - description: '', - percentage: 120, - expirationDate: '2022-12-31', - applicableProducts: [], - }; - - const response = await request(app) - .put(`/api/v1/coupons/${couponId}`) - .set('Authorization', `Bearer ${token}`) - .send(invalidUpdateData); - - expect(response.statusCode).toEqual(400); - expect(response.body.errors).toBeDefined(); - }); - - it('should return a 403 for a user trying to delete a coupon for another user', async () => { - const otherVendorToken = await getVendorToken( - 'email@example.com', - 'Password123', - 'OtherVendor', - 'OtherName' - ) - - const response = await request(app) - .delete(`/api/v1/coupons/${couponId}`) - .set('Authorization', `Bearer ${otherVendorToken}`) - expect(response.statusCode).toEqual(403); - expect(response.body.error).toEqual('You can only delete your own coupons'); - }) - - it('should delete a coupon by ID', async () => { - const response = await request(app) - .delete(`/api/v1/coupons/${couponId}`) - .set('Authorization', `Bearer ${token}`); - - expect(response.statusCode).toEqual(204); - }); - - it('should return a 404 for a non-existent coupon', async () => { - const nonExistentCouponId = 999; - const response = await request(app) - .delete(`/api/v1/coupons/${nonExistentCouponId}`) - .set('Authorization', `Bearer ${token}`); - - expect(response.statusCode).toEqual(404); - expect(response.body.error).toEqual('Coupon not found'); - }); -}); \ No newline at end of file + let token: string; + let couponId: number; + let productId: number; + + beforeAll(async () => { + token = await getVendorToken(); + }); + + it('should create a new coupon with valid data', async () => { + const categoryData = { + name: 'Category', + description: 'category description', + icon: 'category icon', + }; + + const categoryResponse = await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(categoryData); + + const categoryId = categoryResponse.body.data.id; + const productData = { + name: 'New Product', + 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 productResponse = await request(app) + .post('/api/v1/product') + .set('Authorization', `Bearer ${token}`) + .send(productData); + + expect(productResponse.statusCode).toEqual(201); + expect(productResponse.body.message).toEqual( + 'Product successfully created' + ); + expect(productResponse.body.data).toBeDefined(); + productId = productResponse.body.data.id; + + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .post('/api/v1/coupons') + .set('Authorization', `Bearer ${token}`) + .send(couponData); + + expect(response.statusCode).toEqual(201); + expect(response.body.message).toEqual('Coupon created successfully'); + expect(response.body.data).toBeDefined(); + couponId = response.body.data.id; + }); + + it('should return validation errors for invalid coupon data', async () => { + const invalidCouponData = { + description: '', + percentage: 120, + expirationDate: '2022-12-31', + applicableProducts: [], + }; + + const response = await request(app) + .post('/api/v1/coupons') + .set('Authorization', `Bearer ${token}`) + .send(invalidCouponData); + + expect(response.statusCode).toEqual(400); + expect(response.body.errors).toBeDefined(); + }); + + it('should return a 404 for a non-existent product', async () => { + const nonExistentProductId = 999; + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [nonExistentProductId], + }; + + const response = await request(app) + .post('/api/v1/coupons') + .set('Authorization', `Bearer ${token}`) + .send(couponData); + + expect(response.statusCode).toEqual(400); + expect(response.body.error).toEqual( + `Product with id ${nonExistentProductId} not found` + ); + }); + + it('should return a 404 for a non-existent coupon', async () => { + const nonExistentCouponId = 999; + const response = await request(app).get( + `/api/v1/coupons/${nonExistentCouponId}` + ); + + expect(response.statusCode).toEqual(404); + expect(response.body.error).toEqual('Coupon not found'); + }); + + it('should return a 401 for an unauthenticated user', async () => { + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .post('/api/v1/coupons') + .set('Authorization', 'Invalid Token') + .send(couponData); + expect(response.statusCode).toEqual(401); + }); + + it('should return a 403 for a user trying to create a coupon for another users product', async () => { + const otherVendorToken = await getVendorToken( + 'email@example.com', + 'Password123', + 'OtherVendor', + 'OtherName' + ); + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .post('/api/v1/coupons') + .set('Authorization', `Bearer ${otherVendorToken}`) + .send(couponData); + expect(response.statusCode).toEqual(403); + expect(response.body.error).toEqual( + 'You can only create coupons for your own products' + ); + }); + + it('should retrieve all coupons', async () => { + const response = await request(app).get('/api/v1/coupons'); + + expect(response.statusCode).toEqual(200); + expect(Array.isArray(response.body)).toBeTruthy(); + }); + + it('should retrieve all coupons by vendor', async () => { + const response = await request(app) + .get('/api/v1/coupons/mine') + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(200); + expect(Array.isArray(response.body)).toBeTruthy(); + }); + + it('should retrieve a single coupon by ID', async () => { + const response = await request(app).get(`/api/v1/coupons/${couponId}`); + + expect(response.statusCode).toEqual(200); + expect(response.body).toBeDefined(); + }); + + it('should update a coupon by ID', async () => { + const updatedCouponData = { + description: 'Updated Coupon', + percentage: 20, + expirationDate: '2023-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .put(`/api/v1/coupons/${couponId}`) + .set('Authorization', `Bearer ${token}`) + .send(updatedCouponData); + + expect(response.statusCode).toEqual(200); + expect(response.body).toBeDefined(); + }); + + it('should return a 404 for a non-existent coupon while updating', async () => { + const nonExistentCouponId = 999; + const updatedCouponData = { + description: 'Updated Coupon', + percentage: 20, + expirationDate: '2023-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .put(`/api/v1/coupons/${nonExistentCouponId}`) + .set('Authorization', `Bearer ${token}`) + .send(updatedCouponData); + + expect(response.statusCode).toEqual(404); + expect(response.body.error).toEqual('Coupon not found'); + }); + + it('should return a 403 for a user trying to update a coupon for another user', async () => { + const otherVendorToken = await getVendorToken( + 'email@example.com', + 'Password123', + 'OtherVendor', + 'OtherName' + ); + const couponData = { + description: 'Updated Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .put(`/api/v1/coupons/${couponId}`) + .set('Authorization', `Bearer ${otherVendorToken}`) + .send(couponData); + expect(response.statusCode).toEqual(403); + expect(response.body.error).toEqual( + 'You can only create coupons for your own products' + ); + }); + + it('should return a 401 for an unauthenticated user', async () => { + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .put(`/api/v1/coupons/${couponId}`) + .set('Authorization', 'Invalid Token') + .send(couponData); + expect(response.statusCode).toEqual(401); + }); + + it('should return a 401 for an unauthenticated user', async () => { + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .post('/api/v1/coupons') + .set('Authorization', 'Invalid Token') + .send(couponData); + expect(response.statusCode).toEqual(401); + }); + + it('should return a 404 for a non-existent product', async () => { + const nonExistentProductId = 999; + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [nonExistentProductId], + }; + + const response = await request(app) + .put(`/api/v1/coupons/${couponId}`) + .set('Authorization', `Bearer ${token}`) + .send(couponData); + + expect(response.statusCode).toEqual(400); + expect(response.body.error).toEqual( + `Product with id ${nonExistentProductId} not found` + ); + }); + + it('should return validation errors for invalid update data', async () => { + const invalidUpdateData = { + description: '', + percentage: 120, + expirationDate: '2022-12-31', + applicableProducts: [], + }; + + const response = await request(app) + .put(`/api/v1/coupons/${couponId}`) + .set('Authorization', `Bearer ${token}`) + .send(invalidUpdateData); + + expect(response.statusCode).toEqual(400); + expect(response.body.errors).toBeDefined(); + }); + + it('should return a 403 for a user trying to delete a coupon for another user', async () => { + const otherVendorToken = await getVendorToken( + 'email@example.com', + 'Password123', + 'OtherVendor', + 'OtherName' + ); + + const response = await request(app) + .delete(`/api/v1/coupons/${couponId}`) + .set('Authorization', `Bearer ${otherVendorToken}`); + expect(response.statusCode).toEqual(403); + expect(response.body.error).toEqual('You can only delete your own coupons'); + }); + + it('should delete a coupon by ID', async () => { + const response = await request(app) + .delete(`/api/v1/coupons/${couponId}`) + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(200); + }); + + it('should return a 404 for a non-existent coupon', async () => { + const nonExistentCouponId = 999; + const response = await request(app) + .delete(`/api/v1/coupons/${nonExistentCouponId}`) + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(404); + expect(response.body.error).toEqual('Coupon not found'); + }); +}); diff --git a/src/controller/couponController.ts b/src/controller/couponController.ts index a7b42c48..74355f24 100644 --- a/src/controller/couponController.ts +++ b/src/controller/couponController.ts @@ -9,150 +9,214 @@ import UserModel from '../database/models/userModel'; const couponRepository = dbConnection.getRepository(Coupon); const productRepository = dbConnection.getRepository(Product); +const userRepository = dbConnection.getRepository(UserModel); interface couponRequestBody { - description: string; - percentage: number; - expirationDate: Date; - applicableProducts: number[]; + description: string; + percentage: number; + expirationDate: Date; + applicableProducts: number[]; } const createCouponRules = [ - check('description').isLength({ min: 1 }).withMessage('coupon description is required'), - check('percentage').isInt({ min: 0, max: 100 }).withMessage('percentage must be between 0 and 100'), - check('expirationDate').isDate().withMessage('expiration date must be a valid date'), - check('applicableProducts').isArray().withMessage('applicable products must be an array of product ids'), -] + check('description') + .isLength({ min: 1 }) + .withMessage('coupon description is required'), + check('percentage') + .isInt({ min: 0, max: 100 }) + .withMessage('percentage must be between 0 and 100'), + check('expirationDate') + .isDate() + .withMessage('expiration date must be a valid date'), + check('applicableProducts') + .isArray() + .withMessage('applicable products must be an array of product ids'), +]; class CouponController { - // GET /coupons - public getAllCoupons = errorHandler(async (req: Request, res: Response) => { - const coupons = await couponRepository.find(); - return res.status(200).json(coupons); - }) + // GET /coupons + public getAllCoupons = errorHandler(async (req: Request, res: Response) => { + const coupons = await couponRepository.find({ + select: { + vendor: { + id: true, + firstName: true, + lastName: true, + }, + }, + relations: ['vendor', 'applicableProducts'], + }); + return res.status(200).json(coupons); + }); - public getCouponsByVendor = errorHandler(async (req: Request, res: Response) => { - const vendorId = (req.user as UserModel).id; - const coupons = await couponRepository - .createQueryBuilder('coupon') - .innerJoinAndSelect('coupon.applicableProducts', 'product') - .innerJoinAndSelect('product.vendor', 'vendor') - .where('vendor.id = :vendorId', { vendorId }) - .getMany(); - return res.status(200).json(coupons); - }) + public getCouponsByVendor = errorHandler( + async (req: Request, res: Response) => { + const vendorId = (req.user as UserModel).id; + const coupons = await couponRepository + .createQueryBuilder('coupon') + .innerJoinAndSelect('coupon.applicableProducts', 'product') + .innerJoinAndSelect('product.vendor', 'vendor') + .where('vendor.id = :vendorId', { vendorId }) + .getMany(); + return res.status(200).json(coupons); + } + ); - // POST /coupons - public createCoupon = [ - ...createCouponRules, - errorHandler(async (req: Request, res: Response) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - const { description, percentage, expirationDate, applicableProducts } = req.body as couponRequestBody; - const products: Product[] = [] - for (const productId of applicableProducts) { - const product = await productRepository.findOne({ - where: { id: productId }, - relations: ['vendor'] - }); - if (!product) { - return res.status(400).json({ error: `Product with id ${productId} not found` }); - } - if (product.vendor.id !== (req.user as UserModel).id) { - return res.status(403).json({ error: 'You can only create coupons for your own products' }); - } - products.push(product); - } - let code = crypto.randomBytes(4).toString('hex'); - while (await couponRepository.findOne({ where: { code } })) { - code = crypto.randomBytes(4).toString('hex'); - } - const coupon = new Coupon({ - code, - description, - percentage, - expirationDate, - applicableProducts: products - }); - const newCoupon = await couponRepository.save(coupon); - res.status(201).json({ - message: 'Coupon created successfully', - data: newCoupon - }); - }) - ] - - // GET /coupons/:id - public getCouponById = errorHandler(async(req: Request, res: Response) => { - const { id } = req.params; - const coupon = await couponRepository.findOne({ - where: { id: Number(id) }, - relations: ['applicableProducts'] + // POST /coupons + public createCoupon = [ + ...createCouponRules, + errorHandler(async (req: Request, res: Response) => { + const vendorId = req.user!.id; + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + const { description, percentage, expirationDate, applicableProducts } = + req.body as couponRequestBody; + const products: Product[] = []; + for (const productId of applicableProducts) { + const product = await productRepository.findOne({ + where: { id: productId }, + relations: ['vendor'], }); - if (!coupon) { - return res.status(404).json({ error: 'Coupon not found' }); - } else { - return res.status(200).json(coupon); + if (!product) { + return res + .status(400) + .json({ error: `Product with id ${productId} not found` }); + } + if (product.vendor.id !== (req.user as UserModel).id) { + return res.status(403).json({ + error: 'You can only create coupons for your own products', + }); } - }) + products.push(product); + } + let code = crypto.randomBytes(4).toString('hex'); + while (await couponRepository.findOne({ where: { code } })) { + code = crypto.randomBytes(4).toString('hex'); + } + + const vendor = await userRepository.findOne({ + where: { id: vendorId }, + select: { + id: true, + firstName: true, + lastName: true, + }, + }); - // PUT /coupons/:id - public updateCoupon = [ - ...createCouponRules, - errorHandler(async(req: Request, res: Response) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - const { id } = req.params; - const { description, percentage, expirationDate, applicableProducts } = req.body as couponRequestBody; - const coupon = await couponRepository.findOne({ - where: { id: Number(id) }, - }); - if (!coupon) { - return res.status(404).json({ error: 'Coupon not found' }); - } - coupon.description = description; - coupon.percentage = percentage; - coupon.expirationDate = expirationDate; - const products: Product[] = [] - for (const productId of applicableProducts) { - const product = await productRepository.findOne({ - where: { id: productId }, - relations: ['vendor'] - }); - if (!product) { - return res.status(400).json({ error: `Product with id ${productId} not found` }); - } - if (product.vendor.id !== (req.user as UserModel).id) { - return res.status(403).json({ error: 'You can only create coupons for your own products' }); - } - products.push(product); - } - coupon.applicableProducts = products; - const updatedCoupon = await couponRepository.save(coupon) - return res.status(200).json(updatedCoupon); - }) - ] + if (!vendor) { + return res.status(404).json({ message: 'Vendor not found' }); + } + const coupon = new Coupon({ + code, + description, + percentage, + expirationDate, + applicableProducts: products, + vendor: vendor, + }); + const newCoupon = await couponRepository.save(coupon); + res.status(201).json({ + message: 'Coupon created successfully', + data: newCoupon, + }); + }), + ]; - // DELETE /coupons/:id - public deleteCoupon = errorHandler(async(req: Request, res: Response) => { - const { id } = req.params; - const deletedCoupon = await couponRepository.findOne({ - where: { id: Number(id) }, - relations: ['applicableProducts', 'applicableProducts.vendor'] + // GET /coupons/:id + public getCouponById = errorHandler(async (req: Request, res: Response) => { + const { id } = req.params; + const coupon = await couponRepository.findOne({ + where: { id: Number(id) }, + select: { + vendor: { + id: true, + firstName: true, + lastName: true, + }, + applicableProducts: { + vendor: { + id: true, + firstName: true, + lastName: true, + }, + }, + }, + relations: ['vendor', 'applicableProducts'], + }); + if (!coupon) { + return res.status(404).json({ error: 'Coupon not found' }); + } else { + return res.status(200).json(coupon); + } + }); + + // PUT /coupons/:id + public updateCoupon = [ + ...createCouponRules, + errorHandler(async (req: Request, res: Response) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + const { id } = req.params; + const { description, percentage, expirationDate, applicableProducts } = + req.body as couponRequestBody; + const coupon = await couponRepository.findOne({ + where: { id: Number(id) }, + }); + if (!coupon) { + return res.status(404).json({ error: 'Coupon not found' }); + } + coupon.description = description; + coupon.percentage = percentage; + coupon.expirationDate = expirationDate; + const products: Product[] = []; + for (const productId of applicableProducts) { + const product = await productRepository.findOne({ + where: { id: productId }, + relations: ['vendor'], }); - if (!deletedCoupon) { - return res.status(404).json({ error: 'Coupon not found' }); + if (!product) { + return res + .status(400) + .json({ error: `Product with id ${productId} not found` }); } - if (deletedCoupon.applicableProducts[0].vendor.id !== (req.user as UserModel).id) { - return res.status(403).json({ error: 'You can only delete your own coupons' }); - } else { - await couponRepository.delete({ id: Number(id) }); - return res.status(204).end(); + if (product.vendor.id !== (req.user as UserModel).id) { + return res.status(403).json({ + error: 'You can only create coupons for your own products', + }); } - }) + products.push(product); + } + coupon.applicableProducts = products; + const updatedCoupon = await couponRepository.save(coupon); + return res.status(200).json(updatedCoupon); + }), + ]; + + // DELETE /coupons/:id + public deleteCoupon = errorHandler(async (req: Request, res: Response) => { + const { id } = req.params; + const deletedCoupon = await couponRepository.findOne({ + where: { id: Number(id) }, + relations: ['applicableProducts', 'applicableProducts.vendor'], + }); + if (!deletedCoupon) { + return res.status(404).json({ error: 'Coupon not found' }); + } + if ( + deletedCoupon.applicableProducts[0].vendor.id !== + (req.user as UserModel).id + ) { + return res + .status(403) + .json({ error: 'You can only delete your own coupons' }); + } else { + await couponRepository.delete({ id: Number(id) }); + return res.status(200).json({ message: 'Coupon deleted successfully!' }); + } + }); } export default CouponController; diff --git a/src/database/models/couponEntity.ts b/src/database/models/couponEntity.ts index 092476bc..0b6034b7 100644 --- a/src/database/models/couponEntity.ts +++ b/src/database/models/couponEntity.ts @@ -1,42 +1,47 @@ import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToMany, - JoinTable - } from 'typeorm'; - import Product from './productEntity'; - - @Entity() - export default class Coupon { - @PrimaryGeneratedColumn() - id: number; - - @Column() - percentage: number; - - @Column() - code: string; - - @Column('date') - expirationDate: Date; - - @ManyToMany(() => Product) - @JoinTable() - applicableProducts: Product[]; - - @Column({ length: 250 }) - description: string; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; - - constructor(coupon: Partial) { - Object.assign(this, coupon); - } - } \ No newline at end of file + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToMany, + JoinTable, + ManyToOne, +} from 'typeorm'; +import Product from './productEntity'; +import UserModel from './userModel'; + +@Entity() +export default class Coupon { + @PrimaryGeneratedColumn() + id: number; + + @Column() + percentage: number; + + @Column() + code: string; + + @Column('date') + expirationDate: Date; + + @ManyToMany(() => Product) + @JoinTable() + applicableProducts: Product[]; + + @Column({ length: 250 }) + description: string; + + @ManyToOne(() => UserModel, { onDelete: 'CASCADE', nullable: false }) + vendor: UserModel; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + constructor(coupon: Partial) { + Object.assign(this, coupon); + } +} diff --git a/src/docs/couponDocs.ts b/src/docs/couponDocs.ts index 9ae05869..86536947 100644 --- a/src/docs/couponDocs.ts +++ b/src/docs/couponDocs.ts @@ -25,7 +25,7 @@ * expirationDate: * type: string * format: date - * percentage: + * percentage: * type: number * applicableProducts: * type: array @@ -160,7 +160,7 @@ * expirationDate: * type: string * format: date - * percentage: + * percentage: * type: number * applicableProducts: * type: array @@ -231,7 +231,7 @@ * required: true * description: ID of the coupon to delete * responses: - * '200': + * '204': * description: Delete Successful * '404': * description: Coupon not found diff --git a/src/routes/couponRoute.ts b/src/routes/couponRoute.ts index 5aa4465c..fdca873e 100644 --- a/src/routes/couponRoute.ts +++ b/src/routes/couponRoute.ts @@ -1,22 +1,25 @@ import express from 'express'; import CouponController from '../controller/couponController'; import { IsLoggedIn } from '../middlewares/isLoggedIn'; - +import { checkRole } from '../middlewares/authorize'; const couponRouter = express.Router(); const controller = new CouponController(); -couponRouter.route('/') - .get(controller.getAllCoupons) - .post(IsLoggedIn, controller.createCoupon); +couponRouter + .route('/') + .get(controller.getAllCoupons) + .post(IsLoggedIn, checkRole(['Vendor']), controller.createCoupon); -couponRouter.route('/mine') - .get(IsLoggedIn, controller.getCouponsByVendor); +couponRouter + .route('/mine') + .get(IsLoggedIn, checkRole(['Vendor']), controller.getCouponsByVendor); -couponRouter.route('/:id') - .get(controller.getCouponById) - .put(IsLoggedIn, controller.updateCoupon) - .delete(IsLoggedIn, controller.deleteCoupon); +couponRouter + .route('/:id') + .get(controller.getCouponById) + .put(IsLoggedIn, checkRole(['Vendor', 'Admin']), controller.updateCoupon) + .delete(IsLoggedIn, checkRole(['Vendor', 'Admin']), controller.deleteCoupon); -export default couponRouter; \ No newline at end of file +export default couponRouter;