From a1892bf4cece7ea5d3c1f7c81a6fc4bef0ff8efe Mon Sep 17 00:00:00 2001 From: NIYOMUGABO BERNARD <85235653+niyobern@users.noreply.github.com> Date: Mon, 27 May 2024 09:28:41 +0200 Subject: [PATCH] created coupons (#99) --- .github/PULL_REQUEST_TEMPLATE.MD | 9 - package-lock.json | 13 + package.json | 6 +- src/__test__/cartController.test.ts | 123 +++++++- src/__test__/coupon.test.ts | 332 ++++++++++++++++++++++ src/__test__/testSetup.ts | 8 + src/app.ts | 2 +- src/config/db.ts | 2 +- src/controller/cartController.ts | 150 ++++++++-- src/controller/couponController.ts | 158 ++++++++++ src/controller/productController.ts | 1 + src/controller/userController.ts | 1 + src/database/models/cartEntity.ts | 2 +- src/database/models/couponEntity.ts | 42 +++ src/database/models/orderDetailsEntity.ts | 23 ++ src/database/models/orderEntity.ts | 44 +++ src/database/models/userModel.ts | 13 +- src/docs/cartDocs.ts | 89 ++++++ src/docs/couponDocs.ts | 240 ++++++++++++++++ src/docs/couponDoct.ts | 223 +++++++++++++++ src/middlewares/errorHandler.ts | 1 + src/middlewares/isLoggedIn.ts | 2 +- src/routes/cartRoutes.ts | 2 - src/routes/checkoutRoutes.ts | 19 ++ src/routes/couponRoute.ts | 22 ++ src/routes/index.ts | 2 + src/routes/userRoutes.ts | 2 +- src/utilis/couponCalculator.ts | 18 ++ src/utilis/sendEmail.ts | 22 ++ src/utils/couponCalculator.ts | 19 ++ 30 files changed, 1538 insertions(+), 52 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.MD create mode 100644 src/__test__/coupon.test.ts create mode 100644 src/controller/couponController.ts create mode 100644 src/database/models/couponEntity.ts create mode 100644 src/database/models/orderDetailsEntity.ts create mode 100644 src/database/models/orderEntity.ts create mode 100644 src/docs/couponDocs.ts create mode 100644 src/docs/couponDoct.ts create mode 100644 src/routes/checkoutRoutes.ts create mode 100644 src/routes/couponRoute.ts create mode 100644 src/utilis/couponCalculator.ts create mode 100644 src/utilis/sendEmail.ts create mode 100644 src/utils/couponCalculator.ts diff --git a/.github/PULL_REQUEST_TEMPLATE.MD b/.github/PULL_REQUEST_TEMPLATE.MD deleted file mode 100644 index 78072be6..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.MD +++ /dev/null @@ -1,9 +0,0 @@ -#### What does this PR do? - -#### Description of Task to be completed? - -#### How should this be manually tested? - -#### Any background context you want to provide? - -#### What are the relevant jira ticket? diff --git a/package-lock.json b/package-lock.json index 90a2c4fa..16615fa2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "passport-facebook": "^3.0.0", "passport-google-oauth": "^2.0.0", "pg": "^8.11.5", + "stripe": "^15.8.0", "supertest": "^7.0.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", @@ -9088,6 +9089,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-15.8.0.tgz", + "integrity": "sha512-7eEPMgehd1I16cXeP7Rcn/JKkPWIadB9vGIeE+vbCzQXaY5R95AoNmkZx0vmlu1H4QIDs7j1pYIKPRm9Dr4LKg==", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/superagent": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", diff --git a/package.json b/package.json index 33be813c..caa8ad02 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "passport-facebook": "^3.0.0", "passport-google-oauth": "^2.0.0", "pg": "^8.11.5", + "stripe": "^15.8.0", "supertest": "^7.0.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", @@ -67,7 +68,8 @@ "coveragePathIgnorePatterns": [ "/node_modules/", "/src/emails/", - "/src/middlewares/" + "/src/middlewares/", + "/src/emails/" ], "testPathIgnorePatterns": [ "/node_modules/", @@ -103,4 +105,4 @@ "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" } -} \ No newline at end of file +} diff --git a/src/__test__/cartController.test.ts b/src/__test__/cartController.test.ts index 7f8fce5b..d2b330a0 100644 --- a/src/__test__/cartController.test.ts +++ b/src/__test__/cartController.test.ts @@ -6,6 +6,9 @@ import { getBuyerToken, getVendorToken, } from './testSetup'; +import {Cart} from '../database/models/cartEntity' +import dbConnection from '../database'; +const cartRepository =dbConnection.getRepository(Cart) beforeAll(beforeAllHook); afterAll(afterAllHook); @@ -15,13 +18,14 @@ describe('Cart controller tests', () => { let productId: number; let itemId: number; let categoryId: number; + let orderId: number; beforeAll(async () => { buyerToken = await getBuyerToken(); vendorToken = await getVendorToken(); }); - it('should return cart items and total amount for user', async () => { + it('should return cart items and total amount for the user', async () => { // create a category const categoryData = { name: 'Category4', @@ -159,7 +163,8 @@ describe('Cart controller tests', () => { expect(response.statusCode).toEqual(409); expect(response.body.msg).toEqual('Invalid Quantity'); }); - it('should return 409 if quantity exceeds available quantity while ugating a cart', async () => { + + it('should return 409 if quantity exceeds available quantity while updating a cart', async () => { const exceedQuantity = 50000; const response = await request(app) .patch(`/api/v1/cart/${itemId}`) @@ -190,6 +195,7 @@ describe('Cart controller tests', () => { expect(response.body.msg).toEqual('Cart Item deleted successfully'); expect(response.body.count).toBeGreaterThan(0); }); + it('should remove all items from the cart successfully', async () => { const response = await request(app) .delete('/api/v1/cart') @@ -199,4 +205,115 @@ describe('Cart controller tests', () => { expect(response.body.msg).toEqual('Cart Items deleted successfully'); expect(response.body.count).toBeGreaterThanOrEqual(0); }); -}); + + // New tests for checkout, cancel checkout, and get all orders + + it('should place an order successfully', async () => { + const cartResponse = await request(app) + .post('/api/v1/cart') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: productId, + quantity: 2, + }); + + expect(cartResponse.statusCode).toEqual(201); + expect(cartResponse.body.msg).toEqual('Item added to cart successfully'); + expect(cartResponse.body.cartItem).toBeDefined(); + + const checkoutResponse = await request(app) + .post('/api/v1/checkout') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + deliveryInfo: '123 Delivery St.', + paymentInfo: 'VISA 1234', + couponCode: 'DISCOUNT10', + }); + + expect(checkoutResponse.statusCode).toEqual(201); + expect(checkoutResponse.body.msg).toEqual('Order placed successfully'); + expect(checkoutResponse.body.order).toBeDefined(); + expect(checkoutResponse.body.trackingNumber).toBeDefined(); + orderId = checkoutResponse.body.order.id; + }); + + it('should cancel an order successfully', async () => { + const response = await request(app) + .delete(`/api/v1/checkout/cancel-order/${orderId}`) + .set('Authorization', `Bearer ${buyerToken}`); + + expect(response.statusCode).toEqual(200); + expect(response.body.msg).toEqual('Order canceled successfully'); + }); + + it('should return 404 if order is not found while canceling', async () => { + const nonExistentOrderId = 9999; + const response = await request(app) + .delete(`/api/v1/checkout/cancel-order/${nonExistentOrderId}`) + .set('Authorization', `Bearer ${buyerToken}`); + + expect(response.statusCode).toEqual(404); + expect(response.body.msg).toEqual('Order not found'); + }); + + + it('should return 401 if user is not found while checking out', async () => { + // Simulate a request with a non-existent user ID + const invalidUserToken = 'Bearer invalid-user-token'; + + const response = await request(app) + .post('/api/v1/checkout') + .set('Authorization', invalidUserToken) + .send({ + deliveryInfo: '123 Delivery St.', + paymentInfo: 'VISA 1234', + couponCode: 'DISCOUNT10', + }); + + expect(response.statusCode).toEqual(401); + expect(response.body.msg).toBeUndefined(); + }); + + + + + it('should return all orders', async () => { + const response = await request(app) + .get('/api/v1/checkout/getall-order') + .set('Authorization', `Bearer ${buyerToken}`); + + expect(response.statusCode).toEqual(200); + expect(response.body.orders).toBeDefined(); + }); + + + + + it('should return 400 if cart is empty while checking out', async () => { + // Clear the cart before attempting to checkout + await cartRepository.delete({}); + + const response = await request(app) + .post('/api/v1/checkout') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + deliveryInfo: '123 Delivery St.', + paymentInfo: 'VISA 1234', + couponCode: 'DISCOUNT10', + }); + + expect(response.statusCode).toEqual(400); + expect(response.body.msg).toEqual('Cart is empty'); + }); + + + it('should delete all orders', async () => { + const response = await request(app) + .delete('/api/v1/checkout/removeall-order') + .set('Authorization', `Bearer ${buyerToken}`); + + expect(response.statusCode).toEqual(200); + expect(response.body.msg).toEqual('All orders deleted successfully'); + }); + +}) diff --git a/src/__test__/coupon.test.ts b/src/__test__/coupon.test.ts new file mode 100644 index 00000000..600546e9 --- /dev/null +++ b/src/__test__/coupon.test.ts @@ -0,0 +1,332 @@ +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', + }; + + 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'); + }); +}); diff --git a/src/__test__/testSetup.ts b/src/__test__/testSetup.ts index 6dd936e4..e979129d 100644 --- a/src/__test__/testSetup.ts +++ b/src/__test__/testSetup.ts @@ -6,6 +6,7 @@ import Product from '../database/models/productEntity'; import { Cart } from '../database/models'; import request from 'supertest'; import app from '../app'; +import {Order} from '../database/models/orderEntity'; export async function beforeAllHook() { await DbConnection.instance.initializeDb(); @@ -15,12 +16,15 @@ export async function beforeAllHook() { const categoryRepository = DbConnection.connection.getRepository(Category); const productRepository = DbConnection.connection.getRepository(Product); const cartRepository = DbConnection.connection.getRepository(Cart); + const orderRepository =DbConnection.connection.getRepository(Order) await userRepository.createQueryBuilder().delete().execute(); await roleRepository.createQueryBuilder().delete().execute(); await categoryRepository.createQueryBuilder().delete().execute(); await productRepository.createQueryBuilder().delete().execute(); await cartRepository.createQueryBuilder().delete().execute(); + await orderRepository.createQueryBuilder().delete().execute(); + } export async function getAdminToken() { const userRepository = await DbConnection.connection.getRepository(UserModel); @@ -149,12 +153,16 @@ export async function afterAllHook() { const categoryRepository = transactionManager.getRepository(Category); const productRepository = transactionManager.getRepository(Product); const cartRepository = transactionManager.getRepository(Cart); + const orderRepository = transactionManager.getRepository(Order); + await userRepository.createQueryBuilder().delete().execute(); await roleRepository.createQueryBuilder().delete().execute(); await categoryRepository.createQueryBuilder().delete().execute(); await productRepository.createQueryBuilder().delete().execute(); await cartRepository.createQueryBuilder().delete().execute(); + await orderRepository.createQueryBuilder().delete().execute(); + }); await DbConnection.instance.disconnectDb(); } diff --git a/src/app.ts b/src/app.ts index f4a9b804..b6cf26ff 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,7 +5,6 @@ import swaggerUi from 'swagger-ui-express'; import swaggerSpec from './docs/swaggerconfig'; import 'reflect-metadata'; import router from './routes/index'; - import fs from 'fs'; import path from 'path'; import authRoutes from './routes/auth-routes'; @@ -79,6 +78,7 @@ app.get('/', (req: Request, res: Response) => { // Middleware to handle all endpoint routes app.use('/api/v1', router); app.use('/api/v1', userRouter); + // Endpoints for serving social login app.use('/auth', authRoutes); diff --git a/src/config/db.ts b/src/config/db.ts index e2f35383..dafc83ab 100644 --- a/src/config/db.ts +++ b/src/config/db.ts @@ -47,4 +47,4 @@ const config: { production, }; -export default config[env]; +export default config[env]; \ No newline at end of file diff --git a/src/controller/cartController.ts b/src/controller/cartController.ts index bb631808..b673e450 100644 --- a/src/controller/cartController.ts +++ b/src/controller/cartController.ts @@ -4,28 +4,26 @@ import dbConnection from '../database'; import errorHandler from '../middlewares/errorHandler'; import Product from '../database/models/productEntity'; import UserModel from '../database/models/userModel'; +import applyCoupon from '../utilis/couponCalculator'; +import { Order } from '../database/models/orderEntity'; +import { OrderDetails } from '../database/models/orderDetailsEntity'; const cartRepository = dbConnection.getRepository(Cart); const productRepository = dbConnection.getRepository(Product); const userRepository = dbConnection.getRepository(UserModel); +const orderRepository = dbConnection.getRepository(Order); export const addToCart = errorHandler(async (req: Request, res: Response) => { const { productId, quantity } = req.body; const userId = req.user!.id; const product = await productRepository.findOne({ - where: { - id: productId, - }, + where: { id: productId }, }); const user = await userRepository.findOne({ - where: { - id: userId, - }, - select: { - id: true, - }, + where: { id: userId }, + select: { id: true }, }); if (!product || !user) { @@ -57,19 +55,13 @@ export const getCartItems = errorHandler( const userId = req.user!.id; const user = await userRepository.findOne({ - where: { - id: userId, - }, + where: { id: userId }, }); const cartItems = await cartRepository.find({ - where: { - user: user!, - }, + where: { user: user! }, select: { - user: { - id: true, - }, + user: { id: true }, product: { id: true, name: true, @@ -96,9 +88,7 @@ export const updateQuantity = errorHandler( const { quantity } = req.body; const cartItem = await cartRepository.findOne({ - where: { - id: id, - }, + where: { id: id }, relations: ['user', 'product'], }); @@ -127,9 +117,7 @@ export const updateQuantity = errorHandler( export const removeItem = errorHandler(async (req: Request, res: Response) => { const itemId: number = parseInt(req.params.itemId); - const cartItem = await cartRepository.findOne({ - where: { id: itemId }, - }); + const cartItem = await cartRepository.findOne({ where: { id: itemId } }); if (!cartItem) { return res.status(404).json({ msg: 'Item not found' }); @@ -145,11 +133,7 @@ export const removeItem = errorHandler(async (req: Request, res: Response) => { export const removeAllItems = errorHandler( async (req: Request, res: Response) => { const userId = req.user!.id; - const user = await userRepository.findOne({ - where: { - id: userId, - }, - }); + const user = await userRepository.findOne({ where: { id: userId } }); const deletedItems = await cartRepository.delete({ user: user! }); @@ -159,3 +143,111 @@ export const removeAllItems = errorHandler( }); } ); + +export const checkout = errorHandler(async (req: Request, res: Response) => { + const { deliveryInfo, paymentInfo, couponCode } = req.body; + const userId = req.user?.id; + + const user = await userRepository.findOne({ where: { id: userId } }); + if (!user) { + return res.status(404).json({ msg: 'User not found' }); + } + + const cartItems = await cartRepository.find({ + where: { user: { id: userId } }, + relations: ['product'], + }); + + if (cartItems.length === 0) { + return res.status(400).json({ msg: 'Cart is empty' }); + } + + let totalAmount = 0; + const orderDetails: OrderDetails[] = []; + + for (const item of cartItems) { + const product = item.product; + + let price = product.salesPrice * item.quantity; + + // Apply any applicable coupon for each product + if (couponCode) { + price = await applyCoupon(product, couponCode, price); + } + + totalAmount += price; + + const orderDetail = new OrderDetails(); + orderDetail.product = product; + orderDetail.quantity = item.quantity; + orderDetail.price = price; + + orderDetails.push(orderDetail); + } + + const trackingNumber = `Tr${Math.random().toString().slice(2, 8)}`; + + const order = new Order(); + order.user = user; + order.totalAmount = totalAmount; + order.deliveryInfo = deliveryInfo; + order.paymentInfo = paymentInfo; + order.trackingNumber = trackingNumber; + order.orderDetails = orderDetails; + + const savedOrder = await orderRepository.save(order); + + await cartRepository.delete({ user: { id: userId } }); + + return res.status(201).json({ + msg: 'Order placed successfully', + order: savedOrder, + trackingNumber, + user: { + id: user.id, + email: user.email, + }, + }); +}); + +export const deleteAllOrders = errorHandler( + async (req: Request, res: Response) => { + const deletedOrders = await orderRepository.delete({}); + + return res.status(200).json({ + msg: 'All orders deleted successfully', + count: deletedOrders.affected, + }); + } +); + +export const getAllOrders = errorHandler( + async (req: Request, res: Response) => { + const orders = await orderRepository.find({ relations: ['orderDetails'] }); + return res.status(200).json({ orders }); + } +); + +export const cancelOrder = errorHandler(async (req: Request, res: Response) => { + const orderId: number = parseInt(req.params.orderId); + + const order = await orderRepository.findOne({ + where: { id: orderId }, + relations: ['orderDetails'], + }); + + if (!order) { + return res.status(404).json({ msg: 'Order not found' }); + } + + for (const orderDetail of order.orderDetails) { + if (orderDetail.product) { + orderDetail.product.quantity += orderDetail.quantity; + await dbConnection.getRepository('Product').save(orderDetail.product); + } + } + + await orderRepository.remove(order); + + return res.status(200).json({ msg: 'Order canceled successfully' }); +}); diff --git a/src/controller/couponController.ts b/src/controller/couponController.ts new file mode 100644 index 00000000..a7b42c48 --- /dev/null +++ b/src/controller/couponController.ts @@ -0,0 +1,158 @@ +import { Request, Response } from 'express'; +import Coupon from '../database/models/couponEntity'; +import dbConnection from '../database'; +import errorHandler from '../middlewares/errorHandler'; +import { check, validationResult } from 'express-validator'; +import Product from '../database/models/productEntity'; +import crypto from 'crypto'; +import UserModel from '../database/models/userModel'; + +const couponRepository = dbConnection.getRepository(Coupon); +const productRepository = dbConnection.getRepository(Product); + +interface couponRequestBody { + 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'), +] +class CouponController { + // GET /coupons + public getAllCoupons = errorHandler(async (req: Request, res: Response) => { + const coupons = await couponRepository.find(); + 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'] + }); + 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 (!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); + }) + ] + + // 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(204).end(); + } + }) +} + +export default CouponController; diff --git a/src/controller/productController.ts b/src/controller/productController.ts index 82627010..eb13774b 100644 --- a/src/controller/productController.ts +++ b/src/controller/productController.ts @@ -70,6 +70,7 @@ export const createProduct = [ if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } + const vendorId = req.user!.id; const { diff --git a/src/controller/userController.ts b/src/controller/userController.ts index c3b42da9..9c2ed628 100644 --- a/src/controller/userController.ts +++ b/src/controller/userController.ts @@ -201,6 +201,7 @@ export const verify2FA = errorHandler(async (req: Request, res: Response) => { return res.status(200).json({ token }); }); + // Delete All Users export const deleteAllUsers = async (req: Request, res: Response) => { try { diff --git a/src/database/models/cartEntity.ts b/src/database/models/cartEntity.ts index b59a75ba..c3b623dd 100644 --- a/src/database/models/cartEntity.ts +++ b/src/database/models/cartEntity.ts @@ -28,4 +28,4 @@ export class Cart { @UpdateDateColumn() updatedAt: Date; -} +} \ No newline at end of file diff --git a/src/database/models/couponEntity.ts b/src/database/models/couponEntity.ts new file mode 100644 index 00000000..092476bc --- /dev/null +++ b/src/database/models/couponEntity.ts @@ -0,0 +1,42 @@ +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 diff --git a/src/database/models/orderDetailsEntity.ts b/src/database/models/orderDetailsEntity.ts new file mode 100644 index 00000000..8ee38e43 --- /dev/null +++ b/src/database/models/orderDetailsEntity.ts @@ -0,0 +1,23 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { Order } from './orderEntity'; +import Product from './productEntity'; + +@Entity() +export class OrderDetails { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne(() => Order, (order) => order.orderDetails, { + onDelete: 'CASCADE', + }) + order: Order; + + @ManyToOne(() => Product) + product: Product; + + @Column() + quantity: number; + + @Column() + price: number; +} diff --git a/src/database/models/orderEntity.ts b/src/database/models/orderEntity.ts new file mode 100644 index 00000000..5ab8fe8d --- /dev/null +++ b/src/database/models/orderEntity.ts @@ -0,0 +1,44 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToMany, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; +import UserModel from './userModel'; +import { OrderDetails } from './orderDetailsEntity'; + +@Entity() +export class Order { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne(() => UserModel, { nullable: true }) + user: UserModel | null; + + @Column() + totalAmount: number; + + @Column() + status: string; + + @Column({ nullable: true }) + deliveryInfo: string; + + @Column({ nullable: true }) + paymentInfo: string; + + @Column() + trackingNumber: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + @OneToMany(() => OrderDetails, orderDetails => orderDetails.order, { cascade: true }) + orderDetails: OrderDetails[]; +} diff --git a/src/database/models/userModel.ts b/src/database/models/userModel.ts index f71170e2..8aefe58b 100644 --- a/src/database/models/userModel.ts +++ b/src/database/models/userModel.ts @@ -1,6 +1,12 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToMany, +} from 'typeorm'; import { Role } from './roleEntity'; - +import { Order } from './orderEntity'; @Entity() export default class UserModel { @PrimaryGeneratedColumn() @@ -21,6 +27,9 @@ export default class UserModel { @ManyToOne(() => Role) userType: Role; + @OneToMany(() => Order, (order) => order.user, { cascade: true }) + orders: Order[]; + @Column({ nullable: true }) googleId: string; diff --git a/src/docs/cartDocs.ts b/src/docs/cartDocs.ts index 5322d71e..c78d921a 100644 --- a/src/docs/cartDocs.ts +++ b/src/docs/cartDocs.ts @@ -120,3 +120,92 @@ * '500': * description: Internal Server Error */ + +/** + * @swagger + * /api/v1/order/checkout: + * post: + * summary: Checkout and create an order from the cart items + * tags: [Order] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * deliveryInfo: + * type: object + * description: Delivery information for the order + * example: { "address": "123 Main St", "city": "Anytown", "zip": "12345" } + * paymentInfo: + * type: object + * description: Payment information for the order + * example: { "method": "credit card", "details": "**** **** **** 1234" } + * couponCode: + * type: string + * description: Optional coupon code for discount + * email: + * type: string + * description: Email address for guest users + * firstName: + * type: string + * description: First name for guest users + * lastName: + * type: string + * description: Last name for guest users + * responses: + * 201: + * description: Order placed successfully + * 400: + * description: Bad request, e.g., empty cart, invalid quantity + * 401: + * description: Unauthorized, e.g., not logged in + * 404: + * description: User or product not found + * 500: + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/order/checkout/{orderId}: + * delete: + * summary: Cancel a pending order + * tags: [Order] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: orderId + * required: true + * schema: + * type: integer + * description: ID of the order to cancel + * responses: + * 200: + * description: Order canceled successfully + * 400: + * description: Cannot cancel an order that is not pending + * 404: + * description: Order not found + * 500: + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/order/checkout: + * get: + * summary: Get all orders + * tags: [Order] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Successful retrieval of all orders + * 500: + * description: Internal Server Error + */ diff --git a/src/docs/couponDocs.ts b/src/docs/couponDocs.ts new file mode 100644 index 00000000..9ae05869 --- /dev/null +++ b/src/docs/couponDocs.ts @@ -0,0 +1,240 @@ +/** + * @swagger + * tags: + * name: Coupon + * description: Coupon management + */ + +/** + * @swagger + * /api/v1/coupons/: + * post: + * summary: Create a new coupon + * tags: [Coupon] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * description: + * type: string + * expirationDate: + * type: string + * format: date + * percentage: + * type: number + * applicableProducts: + * type: array + * items: + * type: number + * responses: + * '201': + * description: Coupon successfully created + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating successful coupon creation + * data: + * type: object + * properties: + * id: + * type: integer + * description: The unique identifier of the coupon + * code: + * type: string + * description: The code of the coupon + * discount: + * type: number + * description: The discount value of the coupon + * '400': + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * errors: + * type: array + * items: + * type: object + * properties: + * msg: + * type: string + * description: The error message + * '409': + * description: Conflict - Coupon code already exists + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating the code of coupon already exists + */ + +/** + * @swagger + * /api/v1/coupons/: + * get: + * summary: Get all coupons + * tags: [Coupon] + * responses: + * '200': + * description: Successful + * '404': + * description: Coupons not found + * '500': + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/coupons/mine: + * get: + * summary: Get own coupons + * tags: [Coupon] + * security: + * - bearerAuth: [] + * responses: + * '200': + * description: Successful + * '404': + * description: Coupons not found + * '500': + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/coupons/{couponId}: + * get: + * summary: Get an existing coupon + * tags: [Coupon] + * parameters: + * - in: path + * name: couponId + * type: string + * required: true + * description: ID of the coupon + * responses: + * '200': + * description: Successful + * '404': + * description: Coupon not found + * '500': + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/coupons/{couponId}: + * put: + * summary: Update an existing coupon + * tags: [Coupon] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: couponId + * type: string + * required: true + * description: ID of the coupon + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * description: + * type: string + * expirationDate: + * type: string + * format: date + * percentage: + * type: number + * applicableProducts: + * type: array + * items: + * type: number + * responses: + * '200': + * description: Coupon successfully updated + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating successful coupon updation + * data: + * type: object + * properties: + * id: + * type: integer + * description: The unique identifier of the coupon + * code: + * type: string + * description: The code of the coupon + * discount: + * type: number + * description: The discount value of the coupon + * '400': + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * errors: + * type: array + * items: + * type: object + * properties: + * msg: + * type: string + * description: The error message + * '409': + * description: Conflict - Coupon code already exists + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating the code of coupon already exists + */ + +/** + * @swagger + * /api/v1/coupons/{couponId}: + * delete: + * summary: Delete an existing coupon + * tags: [Coupon] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: couponId + * type: string + * required: true + * description: ID of the coupon to delete + * responses: + * '200': + * description: Delete Successful + * '404': + * description: Coupon not found + * '500': + * description: Internal Server Error + */ diff --git a/src/docs/couponDoct.ts b/src/docs/couponDoct.ts new file mode 100644 index 00000000..36e6696c --- /dev/null +++ b/src/docs/couponDoct.ts @@ -0,0 +1,223 @@ +/** + * @swagger + * tags: + * name: Coupon + * description: Coupon management + */ + +/** + * @swagger + * /api/v1/coupons/: + * post: + * summary: Create a new coupon + * tags: [Coupon] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * description: + * type: string + * expirationDate: + * type: string + * format: date + * percentage: + * type: number + * applicableProducts: + * type: array + * items: + * type: number + * responses: + * '201': + * description: Coupon successfully created + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating successful coupon creation + * data: + * type: object + * properties: + * id: + * type: integer + * description: The unique identifier of the coupon + * code: + * type: string + * description: The code of the coupon + * discount: + * type: number + * description: The discount value of the coupon + * '400': + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * errors: + * type: array + * items: + * type: object + * properties: + * msg: + * type: string + * description: The error message + * '409': + * description: Conflict - Coupon code already exists + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating the code of coupon already exists + */ + +/** + * @swagger + * /api/v1/coupons/: + * get: + * summary: Get all coupons + * tags: [Coupon] + * responses: + * '200': + * description: Successful + * '404': + * description: Coupons not found + * '500': + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/coupons/{couponId}: + * get: + * summary: Get an existing coupon + * tags: [Coupon] + * parameters: + * - in: path + * name: couponId + * type: string + * required: true + * description: ID of the coupon + * responses: + * '200': + * description: Successful + * '404': + * description: Coupon not found + * '500': + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/coupons/{couponId}: + * put: + * summary: Update an existing coupon + * tags: [Coupon] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: couponId + * type: string + * required: true + * description: ID of the coupon + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * description: + * type: string + * expirationDate: + * type: string + * format: date + * percentage: + * type: number + * applicableProducts: + * type: array + * items: + * type: number + * responses: + * '200': + * description: Coupon successfully updated + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating successful coupon updation + * data: + * type: object + * properties: + * id: + * type: integer + * description: The unique identifier of the coupon + * code: + * type: string + * description: The code of the coupon + * discount: + * type: number + * description: The discount value of the coupon + * '400': + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * errors: + * type: array + * items: + * type: object + * properties: + * msg: + * type: string + * description: The error message + * '409': + * description: Conflict - Coupon code already exists + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating the code of coupon already exists + */ + +/** + * @swagger + * /api/v1/coupons/{couponId}: + * delete: + * summary: Delete an existing coupon + * tags: [Coupon] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: couponId + * type: string + * required: true + * description: ID of the coupon to delete + * responses: + * '200': + * description: Delete Successful + * '404': + * description: Coupon not found + * '500': + * description: Internal Server Error + */ diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index 3d741642..a4cd06a2 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -10,6 +10,7 @@ function errorHandler(func: MiddlewareFunction): MiddlewareFunction { try { return await func(req, res); } catch (error) { + // console.log({'Error':error}) const message = (error as { detail?: string }).detail || 'Internal Server Error'; return res.status(500).send(message); diff --git a/src/middlewares/isLoggedIn.ts b/src/middlewares/isLoggedIn.ts index 17fea984..dd7706b3 100644 --- a/src/middlewares/isLoggedIn.ts +++ b/src/middlewares/isLoggedIn.ts @@ -30,4 +30,4 @@ export const IsLoggedIn = (req: Request, res: Response, next: NextFunction) => { } catch (error) { return res.status(401).json({ message: 'Unauthorized: Invalid token' }); } -}; +}; \ No newline at end of file diff --git a/src/routes/cartRoutes.ts b/src/routes/cartRoutes.ts index c1f71902..4e6b3f1a 100644 --- a/src/routes/cartRoutes.ts +++ b/src/routes/cartRoutes.ts @@ -11,10 +11,8 @@ import { } from '../controller/cartController'; const cartRouter = Router(); - cartRouter.use(IsLoggedIn, checkRole(['Buyer'])); cartRouter.route('/').post(addToCart).get(getCartItems).delete(removeAllItems); cartRouter.route('/:itemId').delete(removeItem).patch(updateQuantity); - export default cartRouter; diff --git a/src/routes/checkoutRoutes.ts b/src/routes/checkoutRoutes.ts new file mode 100644 index 00000000..221269b8 --- /dev/null +++ b/src/routes/checkoutRoutes.ts @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import { checkRole } from '../middlewares/authorize'; +import { IsLoggedIn } from '../middlewares/isLoggedIn'; + +import { + checkout, + deleteAllOrders, + getAllOrders, + cancelOrder, +} from '../controller/cartController'; + +const checkoutRoutes = Router(); +checkoutRoutes.use(IsLoggedIn, checkRole(['Buyer'])); +checkoutRoutes.route('/').post(checkout); +checkoutRoutes.route('/removeall-order').delete(deleteAllOrders); +checkoutRoutes.route('/getall-order').get(getAllOrders); +checkoutRoutes.route('/cancel-order/:orderId').delete(cancelOrder); + +export default checkoutRoutes; diff --git a/src/routes/couponRoute.ts b/src/routes/couponRoute.ts new file mode 100644 index 00000000..f2acaa50 --- /dev/null +++ b/src/routes/couponRoute.ts @@ -0,0 +1,22 @@ +import express from 'express'; +import CouponController from '../controller/couponController'; +import { IsLoggedIn } from '../middlewares/isLoggedIn'; + + +const couponRouter = express.Router(); + +const controller = new CouponController(); + +couponRouter.route('/') + .get(controller.getAllCoupons) + .post(IsLoggedIn, controller.createCoupon); + +couponRouter.route('/mine') + .get(IsLoggedIn, controller.getCouponsByVendor); + +couponRouter.route('/:id') + .get(controller.getCouponById) + .put(IsLoggedIn, controller.updateCoupon) + .delete(IsLoggedIn, controller.deleteCoupon); + +export default couponRouter; diff --git a/src/routes/index.ts b/src/routes/index.ts index b11717ea..d310b939 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -5,6 +5,7 @@ import productRoutes from './productRoutes'; import categoryRoutes from './categoryRoutes'; import buyerRoutes from './buyerRoutes'; import cartRoutes from '../routes/cartRoutes'; +import couponRouter from './couponRoute' const router = Router(); @@ -14,5 +15,6 @@ router.use('/product', productRoutes); router.use('/category', categoryRoutes); router.use('/buyer', buyerRoutes); router.use('/cart', cartRoutes); +router.use('/coupons', couponRouter) export default router; diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 1654a043..d58ee8c6 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -39,4 +39,4 @@ userRouter.put( ); userRouter.post('/recover', recoverPassword); userRouter.put('/recover/confirm', updateNewPassword) -export default userRouter; +export default userRouter; \ No newline at end of file diff --git a/src/utilis/couponCalculator.ts b/src/utilis/couponCalculator.ts new file mode 100644 index 00000000..6bbf918d --- /dev/null +++ b/src/utilis/couponCalculator.ts @@ -0,0 +1,18 @@ +import Coupon from '../database/models/couponEntity'; +import Product from '../database/models/productEntity'; +import dbConnection from '../database'; + +const couponRepository = dbConnection.getRepository(Coupon); + +export default async function applyCoupon(product: Product, couponCode: string, price: number): Promise { + const coupon = await couponRepository.findOne({ where: { code: couponCode }, relations: ['applicableProducts'] }); + + if (!coupon) { + return price; + } + + if (!coupon.applicableProducts.find(applicableProduct => applicableProduct.id === product.id)) { + return price; + } + return price * (1 - coupon.percentage); +} diff --git a/src/utilis/sendEmail.ts b/src/utilis/sendEmail.ts new file mode 100644 index 00000000..0eafd075 --- /dev/null +++ b/src/utilis/sendEmail.ts @@ -0,0 +1,22 @@ +import nodemailer from 'nodemailer'; + +const sendEmail = async (data: { email: string; subject: string; html: string }) => { + const transporter = nodemailer.createTransport({ + service: 'Gmail', + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + }); + + const mailOptions = { + from: process.env.EMAIL_USER, + to: data.email, + subject: data.subject, + html: data.html, + }; + + await transporter.sendMail(mailOptions); +}; + +export default sendEmail; diff --git a/src/utils/couponCalculator.ts b/src/utils/couponCalculator.ts new file mode 100644 index 00000000..5f23f470 --- /dev/null +++ b/src/utils/couponCalculator.ts @@ -0,0 +1,19 @@ +import Coupon from '../database/models/couponEntity'; +import Product from '../database/models/productEntity'; +import dbConnection from '../database'; + +const couponRepository = dbConnection.getRepository(Coupon); + +export default async function applyCoupon(product: Product, couponCode: string, price: number): Promise { + const coupon = await couponRepository.findOne({ where: { code: couponCode }, relations: ['applicableProducts'] }); + + if (!coupon) { + return price; + } + + if (!coupon.applicableProducts.find(applicableProduct => applicableProduct.id === product.id)) { + return price; + } + const percentage = coupon.percentage / 100; + return price * (1 - percentage); +} \ No newline at end of file