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 f60c3bf5..3f3ff677 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", @@ -70,7 +71,8 @@ ], "testPathIgnorePatterns": [ "/node_modules/", - "/src/emails/" + "/src/emails/", + "/src/middlewares/" ] }, "devDependencies": { @@ -102,4 +104,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..c78bce48 100644 --- a/src/__test__/cartController.test.ts +++ b/src/__test__/cartController.test.ts @@ -6,6 +6,11 @@ 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,7 +20,6 @@ describe('Cart controller tests', () => { let productId: number; let itemId: number; let categoryId: number; - beforeAll(async () => { buyerToken = await getBuyerToken(); vendorToken = await getVendorToken(); @@ -200,3 +204,136 @@ describe('Cart controller tests', () => { expect(response.body.count).toBeGreaterThanOrEqual(0); }); }); + +describe('Checkout Tests', () => { + let buyerToken: string; + let productId: number; + let orderId: number; + beforeAll(async () => { + buyerToken = await getBuyerToken(); + }); + it('should return a 400 status code if validation errors occur', async () => { + const response = await request(app) + .post('/api/v1/checkout') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + deliveryInfo: '', + paymentInfo: '', + couponCode: 'DISCOUNT10', + }); + + expect(response.status).toBe(400); + expect(response.body.errors).toBeDefined(); + }); + + 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 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 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 index 600546e9..b6d8a618 100644 --- a/src/__test__/coupon.test.ts +++ b/src/__test__/coupon.test.ts @@ -329,4 +329,4 @@ describe('Coupon Controller Tests', () => { expect(response.statusCode).toEqual(404); expect(response.body.error).toEqual('Coupon not found'); }); -}); +}); \ No newline at end of file diff --git a/src/__test__/testSetup.ts b/src/__test__/testSetup.ts index 0d70d148..3c9af3d1 100644 --- a/src/__test__/testSetup.ts +++ b/src/__test__/testSetup.ts @@ -5,7 +5,7 @@ import app from '../app'; export async function beforeAllHook() { await DbConnection.instance.initializeDb(); - await dbConnection.synchronize(true) // This will drop all tables + await dbConnection.synchronize(true); // This will drop all tables } export async function getAdminToken() { const userRepository = await DbConnection.connection.getRepository(UserModel); @@ -129,4 +129,4 @@ export const getBuyerToken = async () => { export async function afterAllHook() { await DbConnection.instance.disconnectDb(); -} \ No newline at end of file +} diff --git a/src/__test__/userController.test.ts b/src/__test__/userController.test.ts index 8ff5259f..875bd365 100644 --- a/src/__test__/userController.test.ts +++ b/src/__test__/userController.test.ts @@ -414,4 +414,84 @@ describe('Get All Users Tests', () => { expect(response.status).toBe(200); expect(response.body.message).toEqual('All users deleted successfully'); }); -}); \ No newline at end of file +}); +describe('update user Profile', () => { + interface IUser { + id: number; + firstName: string; + lastName: string; + email: string; + password?: string; + userType?: Role; + googleId?: string; + facebookId?: string; + picture?: string; + provider?: string; + isVerified: boolean; + twoFactorCode?: number; + } + + interface Role { + id: number; + name: string; + permissions: string[]; + } + + +let user: IUser | undefined | null; +const userData = { +firstName: 'jan', +lastName: 'bosco', +email: 'bosco@gmail.com', +password: 'boscoPassword123', +}; + +beforeEach(async () => { + +await request(app).post('/api/v1/register').send(userData); +user = await userRepository.findOne({ where: { email: userData.email } }); +}); + +it('should update the user profile successfully', async () => { +if (user) { + const newUserData = { + firstName: 'NewFirstName', + lastName: 'NewLastName', + email: 'newemail@example.com', + password: 'bosco@gmail.com', + }; + + const response = await request(app) + .put(`/api/v1/updateProfile/${user?.id}`) + .send(newUserData); + expect(response.statusCode).toBe(201); + expect(response.body.message).toBe('User updated successfully'); +} +}); + +it('should return 404 when user not found', async () => { +const Id = 999; +const response = await request(app) + .put(`/api/v1/updateProfile/${Id}`) + .send(userData); +expect(response.statusCode).toBe(404); +expect(response.body.error).toBe('User not found'); +}); + +it('should return 400 when email already exists', async () => { +if (user) { + const newUserData = { + firstName: 'NewFirstName', + lastName: 'NewLastName', + email: 'newemail@example.com', + password: 'bosco@gmail.com', + }; + + const response = await request(app) + .put(`/api/v1/updateProfile/${user.id}`) + .send(newUserData); + expect(response.statusCode).toBe(400); + expect(response.body.error).toBe('Email is already taken'); +} +}); +}); diff --git a/src/app.ts b/src/app.ts index 79f2c3b6..bb088615 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'; 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..972a030f 100644 --- a/src/controller/cartController.ts +++ b/src/controller/cartController.ts @@ -4,10 +4,30 @@ 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'; +import { check, validationResult } from 'express-validator'; const cartRepository = dbConnection.getRepository(Cart); const productRepository = dbConnection.getRepository(Product); const userRepository = dbConnection.getRepository(UserModel); +const orderRepository = dbConnection.getRepository(Order); + +interface CheckoutRequestBody { + deliveryInfo: string; + paymentInfo: string; + couponCode?: string; +} + +const checkoutRules = [ + check('deliveryInfo') + .isLength({ min: 1 }) + .withMessage('Delivery info is required'), + check('paymentInfo') + .isLength({ min: 1 }) + .withMessage('Payment info is required'), +]; export const addToCart = errorHandler(async (req: Request, res: Response) => { const { productId, quantity } = req.body; @@ -159,3 +179,110 @@ export const removeAllItems = errorHandler( }); } ); + +export const checkout = [ + ...checkoutRules, + errorHandler(async (req: Request, res: Response) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + const { deliveryInfo, paymentInfo, couponCode } = + req.body as CheckoutRequestBody; + const userId = req.user?.id; + + // Fetch the user who is checking out + + const user = await userRepository.findOne({ where: { id: userId } }); + + // Fetch the cart items for this user + 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[] = []; + + // Process each cart item + 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.status = 'Pending'; + 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, + }); + }), +]; + +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' }); + } + + await orderRepository.remove(order); + + return res.status(200).json({ msg: 'Order canceled successfully' }); +}); diff --git a/src/controller/userController.ts b/src/controller/userController.ts index c3b42da9..e2042726 100644 --- a/src/controller/userController.ts +++ b/src/controller/userController.ts @@ -9,6 +9,7 @@ import { sendCode } from '../emails/mailer'; import jwt from 'jsonwebtoken'; import errorHandler from '../middlewares/errorHandler'; + // Assuming dbConnection.getRepository(UserModel) returns a repository instance const userRepository = dbConnection.getRepository(UserModel); const roleRepository = dbConnection.getRepository(Role); @@ -21,6 +22,14 @@ interface CreateUserRequestBody { userType: 'Admin' | 'vendor' | 'buyer'; } +interface UpdateRrofileRequestBody { + firstName: string; + lastName: string; + email: string; + password:string; +} + + // Define validation and sanitization rules const registerUserRules = [ check('firstName').isLength({ min: 1 }).withMessage('First name is required'), @@ -201,6 +210,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 { @@ -265,4 +275,32 @@ await userRepository.save(user); return res.status(200).json({ message: 'Password updated successfully' }); -}); \ No newline at end of file +}); +export const updateProfile = errorHandler(async (req: Request, res: Response) => { + const userId: number = parseInt(req.params.id); + const { firstName, lastName, email } = req.body as UpdateRrofileRequestBody; + + const user = await userRepository.findOne({ where: { id: userId } }); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + user.firstName = firstName || user.firstName; + user.lastName = lastName || user.lastName; + + + const emailExists = await userRepository.findOne({ where: { email } }); + + if (emailExists) { + return res.status(400).json({ error: 'Email is already taken' }); + } + + user.email = email; + + + + await userRepository.save(user); + + return res.status(201).json({ message: 'User updated successfully' }); +}); 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/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..131f0bfa 100644 --- a/src/docs/cartDocs.ts +++ b/src/docs/cartDocs.ts @@ -120,3 +120,107 @@ * '500': * description: Internal Server Error */ + +/** + * @swagger + * /api/v1/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/checkout/cancel-order/{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/checkout/getall-order: + * get: + * summary: Get all orders + * tags: [Order] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Successful retrieval of all orders + * 500: + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/checkout/removeall-order: + * delete: + * summary: Delete all orders + * tags: [Order] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: All orders deleted successfully + * 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/docs/userRegisterDocs.ts b/src/docs/userRegisterDocs.ts index f047b8d6..83cad76d 100644 --- a/src/docs/userRegisterDocs.ts +++ b/src/docs/userRegisterDocs.ts @@ -221,6 +221,83 @@ * type: string * description: An error message indicating a server error */ + +/** + * @swagger + * /api/v1/updateProfile/{id}: + * put: + * summary: Update user profile + * tags: + * - User + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the user to update + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * firstName: + * type: string + * description: The updated first name of the user. + * lastName: + * type: string + * description: The updated last name of the user. + * email: + * type: string + * format: email + * description: The updated email address of the user. + * responses: + * '200': + * description: User profile updated successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: A message indicating successful profile update. + * '400': + * description: Bad request or validation errors. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Error message indicating the reason for the bad request. + * '404': + * description: Not Found - User not found. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: A message indicating the user was not found. + * '500': + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: A message indicating an internal server error occurred. + */ + + + /** * @swagger * /api/v1/user/recover/confirm: @@ -275,4 +352,4 @@ * message: * type: string * description: An error message indicating a server error - */ + */ \ No newline at end of file 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/index.ts b/src/routes/index.ts index d310b939..bdf772b4 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -5,8 +5,8 @@ import productRoutes from './productRoutes'; import categoryRoutes from './categoryRoutes'; import buyerRoutes from './buyerRoutes'; import cartRoutes from '../routes/cartRoutes'; -import couponRouter from './couponRoute' - +import couponRouter from './couponRoute'; +import chekoutRoutes from './checkoutRoutes'; const router = Router(); router.use('/user', userRouter); @@ -15,6 +15,7 @@ router.use('/product', productRoutes); router.use('/category', categoryRoutes); router.use('/buyer', buyerRoutes); router.use('/cart', cartRoutes); -router.use('/coupons', couponRouter) +router.use('/coupons', couponRouter); +router.use('/checkout', chekoutRoutes); export default router; diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 1654a043..6b64745f 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -8,6 +8,7 @@ import { updateNewPassword, getAllUsers, deleteAllUsers, + updateProfile, } from '../controller/userController'; import { @@ -39,4 +40,6 @@ userRouter.put( ); userRouter.post('/recover', recoverPassword); userRouter.put('/recover/confirm', updateNewPassword) + +userRouter.put('/updateProfile/:id',updateProfile); export default userRouter; diff --git a/src/utilis/couponCalculator.ts b/src/utilis/couponCalculator.ts new file mode 100644 index 00000000..5f23f470 --- /dev/null +++ b/src/utilis/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 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;