From 3a2e5ef0f65ab6953b82d1d0405908e709f5ae31 Mon Sep 17 00:00:00 2001 From: 13XAVI Date: Wed, 29 May 2024 20:49:26 +0200 Subject: [PATCH] adde some file added coverages fix(review-swagger-docs): fix swagger documentation of thereview task (#114) - add missing security tag in review docs [Fixes #113] adde some file added some tests added coverages fix(review-swagger-docs): fix swagger documentation of thereview task (#114) - add missing security tag in review docs [Fixes #113] adde some file * fix(create-review): fix failing create review function -add validation before processing data [Fixes #116] * fix(create-review): fix failing create review function -add validation before processing data [Fixes #116] dockerizing project by creating containers for app & postgres (#110) fix minor issue in deployment of render latest commit Update Readme file to include Docker-specific information Update Readme file to include Docker-specific information * fix(create-review): fix failing create review function -add validation before processing data [Fixes #116] * fix(create-review): fix failing create review function -add validation before processing data [Fixes #116] dockerizing project by creating containers for app & postgres (#110) fix minor issue in deployment of render latest commit Update Readme file to include Docker-specific information Update Readme file to include Docker-specific information fix(review-swagger-docs): fix swagger documentation of thereview task (#114) (#115) - add missing security tag in review docs [Fixes #113] update profile (#72) (#104) review controller adding testing fix lint issue update profile (#72) (#104) review controller adding testing fix lint issue Co-authored-by: Joslyn Manzi Karenzi fix(stripe-payment): fix minor issue in stripe payment (#120) - add status check on the stripe response before setting order.paid to true [Fixes #119] --- .dockerignore | 5 + Dockerfile | 11 ++ README.md | 17 ++ docker-compose.yml | 22 +++ package-lock.json | 42 ++++- package.json | 2 +- src/__test__/buyerWishlist.test.ts | 143 ++++++++++++++ src/__test__/orderController.test.ts | 76 ++++++++ src/__test__/payment.test.ts | 198 ++++++++++--------- src/__test__/review.test.ts | 9 + src/config/db.ts | 2 +- src/controller/buyerController.ts | 70 ++++--- src/controller/buyerWishlistController.ts | 157 +++++++++++++++ src/controller/orderController.ts | 49 +++++ src/controller/reviewController.ts | 13 +- src/database/models/buyerWishList.ts | 26 +++ src/database/models/orderEntity.ts | 8 +- src/docs/buyerDocs.ts | 210 +++++++++++++++------ src/docs/buyerWishlist.ts | 220 ++++++++++++++++++++++ src/docs/cartDocs.ts | 59 ++++++ src/docs/reviewDocs.ts | 42 ++--- src/middlewares/createReviewSchema.ts | 8 + src/routes/buyerRoutes.ts | 12 +- src/routes/index.ts | 4 +- src/routes/orderRoutes.ts | 9 + 25 files changed, 1193 insertions(+), 221 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 src/__test__/buyerWishlist.test.ts create mode 100644 src/__test__/orderController.test.ts create mode 100644 src/controller/buyerWishlistController.ts create mode 100644 src/controller/orderController.ts create mode 100644 src/database/models/buyerWishList.ts create mode 100644 src/docs/buyerWishlist.ts create mode 100644 src/middlewares/createReviewSchema.ts create mode 100644 src/routes/orderRoutes.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..90f069ce --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +/node_modules +/coverage +Dockerfile +.dockerignore +docker-compose.yaml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..93fc512b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:latest + +WORKDIR /usr/src/app + +COPY package*.json ./ +RUN npm install + +COPY . . + +EXPOSE 3000 +CMD ["npm", "run", "dev"] diff --git a/README.md b/README.md index 48dba2f1..28312855 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,23 @@ To get started with Dynamites API, follow these simple steps: npm run test:ci ``` +## Docker + +Before you run that commands you must have docker installed in your PC + +1. **Build the Docker Image:** + ```sh + docker build -t . + ``` +2. **Use Docker Compose to run Containers :** + ```sh + docker-compose up + ``` +3. **Stop the Running Containers:** + - If running with Docker Compose: + ```sh + docker-compose down + ``` ## Usage Once the development server is running, you can interact with the API using HTTP requests. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..dae5931b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3' +services: + db: + image: postgres + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD_DEV} + POSTGRES_USER: ${DB_USER_DEV} + POSTGRES_DB: ${DB_NAME_DEV} + ports: + - '5431:5432' + + app: + build: . + ports: + - '8080:3000' + depends_on: + - db + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules + env_file: + - .env diff --git a/package-lock.json b/package-lock.json index 16615fa2..aa3d2039 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,11 +22,11 @@ "express": "^4.19.2", "express-validator": "^7.0.1", "handlebars": "^4.7.8", - "jest": "^29.7.0", "joi": "^17.13.1", "jsonwebtoken": "^9.0.2", "mailgun-js": "^0.22.0", "morgan": "^1.10.0", + "nock": "^13.5.4", "nodemailer": "^6.9.13", "nodemon": "^3.1.0", "otplib": "^12.0.1", @@ -66,7 +66,7 @@ "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", "prettier": "^3.2.5", - "ts-jest": "^29.1.2", + "ts-jest": "^29.1.4", "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" } @@ -6667,6 +6667,11 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -7210,6 +7215,19 @@ "node": ">= 0.4.0" } }, + "node_modules/nock": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", + "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -8212,6 +8230,14 @@ "node": ">= 6" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "engines": { + "node": ">= 8" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -9451,9 +9477,9 @@ } }, "node_modules/ts-jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", - "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "version": "29.1.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.4.tgz", + "integrity": "sha512-YiHwDhSvCiItoAgsKtoLFCuakDzDsJ1DLDnSouTaTmdOcOwIkSzbLXduaQ6M5DRVhuZC/NYaaZ/mtHbWMv/S6Q==", "dev": true, "dependencies": { "bs-logger": "0.x", @@ -9469,10 +9495,11 @@ "ts-jest": "cli.js" }, "engines": { - "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", "@jest/types": "^29.0.0", "babel-jest": "^29.0.0", "jest": "^29.0.0", @@ -9482,6 +9509,9 @@ "@babel/core": { "optional": true }, + "@jest/transform": { + "optional": true + }, "@jest/types": { "optional": true }, diff --git a/package.json b/package.json index 3f3ff677..093c4573 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "format": "prettier --write .", "test": "cross-env NODE_ENV=test jest --runInBand --no-cache --detectOpenHandles", "test:ci": "cross-env NODE_ENV=test jest --runInBand --coverage --detectOpenHandles" - }, + }, "repository": { "type": "git", "url": "git+https://github.com/atlp-rwanda/dynamites-ecomm-be.git" diff --git a/src/__test__/buyerWishlist.test.ts b/src/__test__/buyerWishlist.test.ts new file mode 100644 index 00000000..084b07b3 --- /dev/null +++ b/src/__test__/buyerWishlist.test.ts @@ -0,0 +1,143 @@ +import request from 'supertest'; +import app from '../app'; +import { afterAllHook, beforeAllHook } from './testSetup'; +import { getBuyerToken, getVendorToken } from './testSetup'; + +beforeAll(beforeAllHook); +afterAll(afterAllHook); +export let buyerToken: string; +let vendorToken: string; +let productId: number; +let categoryId: number; + +beforeAll(async () => { + buyerToken = await getBuyerToken(); + vendorToken = await getVendorToken(); + + const categoryData = { + name: 'Category4', + description: 'category description', + }; + + const categoryResponse = await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${vendorToken}`) + .send(categoryData); + + categoryId = categoryResponse.body.data.id; + + const productData = { + name: 'New Product Two', + image: 'new_product.jpg', + gallery: [], + shortDesc: 'This is a new product', + longDesc: 'Detailed description of the new product', + categoryId: categoryId, + quantity: 10, + regularPrice: 5, + salesPrice: 4, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + }; + + const response = await request(app) + .post('/api/v1/product') + .set('Authorization', `Bearer ${vendorToken}`) + .send(productData); + + productId = response.body.data.id; + + const getResponse = await request(app) + .get(`/api/v1/buyer/get_product/${productId}`) + .set('Authorization', `Bearer ${buyerToken}`); + + expect(getResponse.statusCode).toEqual(200); + expect(getResponse.body.msg).toEqual('Product retrieved successfully'); +}); + +describe('POST /api/v1/buyer/addItemToWishList', () => { + it('should add an item to the wishlist', async () => { + const res = await request(app) + .post('/api/v1/buyer/addItemToWishList') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: productId, + time: '2024-05-21T12:00:00Z', + }); + + expect(res.statusCode).toEqual(201); + expect(res.body.message).toContain('Wishlist successfully created'); + }); + + it('should not allow adding an item already in the wishlist', async () => { + await request(app) + .post('/api/v1/buyer/addItemToWishList') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: productId, + time: '2024-05-21T12:00:00Z', + }); + + const res = await request(app) + .post('/api/v1/buyer/addItemToWishList') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: productId, + time: '2024-05-21T12:00:00Z', + }); + + expect(res.statusCode).toEqual(409); + expect(res.body.message).toContain('Product is already in the wishlist'); + }); +}); + +describe('DELETE /api/v1/buyer/removeToWishList', () => { + it('should remove a product from the wishlist', async () => { + const res = await request(app) + .delete('/api/v1/buyer/removeToWishList') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: productId, + }); + + expect(res.statusCode).toEqual(200); + expect(res.body.message).toContain( + 'Product successfully removed from wishlist' + ); + }); +}); + +describe('GET /api/v1/buyer/getWishList', () => { + it('should get all wishlists', async () => { + const res = await request(app) + .get('/api/v1/buyer/getWishList') + .set('Authorization', `Bearer ${buyerToken}`); + + expect(res.statusCode).toEqual(200); + expect(res.body.message).toContain('Data retrieved successfully'); + }); +}); +describe('GET /api/v1/buyer/getOneWishList', () => { + it('should get all wishlists', async () => { + const res = await request(app) + .get('/api/v1/buyer/getOneWishList') + .set('Authorization', `Bearer ${buyerToken}`); + + expect(res.statusCode).toEqual(200); + expect(res.body.message).toContain('Data retrieved successfully'); + }); +}); + +describe('RemoveProductFromWishList', () => { + it('should return an error when the wishlist or product is not found', async () => { + const res = await request(app) + .delete('/api/v1/buyer/removeToWishList') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: 9999, + }); + expect(res.statusCode).toEqual(404); + expect(res.body.message).toContain('Product not found in wishlist'); + }); +}); diff --git a/src/__test__/orderController.test.ts b/src/__test__/orderController.test.ts new file mode 100644 index 00000000..003470e4 --- /dev/null +++ b/src/__test__/orderController.test.ts @@ -0,0 +1,76 @@ +import request from 'supertest'; +import app from '../app'; +import { Order } from '../database/models/orderEntity'; +import { afterAllHook, beforeAllHook } from './testSetup'; +import dbConnection from '../database'; + +const orderRepository = dbConnection.getRepository(Order); + +beforeAll(beforeAllHook); +afterAll(afterAllHook); + +describe('Order Routes', () => { + describe('PUT /order/:orderId', () => { + it('should update order status to Failed', async () => { + const order = orderRepository.create({ + status: 'Pending', + totalAmount: 40, + trackingNumber: '34343653', + }); + await orderRepository.save(order); + + const response = await request(app) + .put(`/api/v1/order/${order.id}`) + .send({ status: 'Failed' }); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ msg: 'Order status updated to Failed' }); + }); + + it('should return 400 if orderId is invalid', async () => { + const response = await request(app) + .put('/api/v1/order/invalid') + .send({ status: 'Failed' }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ msg: 'Invalid orderId' }); + }); + + it('should return 400 if status is invalid', async () => { + const order = orderRepository.create({ + status: 'Pending', + totalAmount: 40, + trackingNumber: '34343653', + }); + await orderRepository.save(order); + + const response = await request(app) + .put(`/api/v1/order/${order.id}`) + .send({ status: 'InvalidStatus' }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ msg: 'Invalid status' }); + }); + + it('should return 404 if order is not found', async () => { + const response = await request(app) + .put('/api/v1/order/9999') + .send({ status: 'Failed' }); + + expect(response.status).toBe(404); + expect(response.body).toEqual({ msg: 'Order Not Found' }); + }); + + it('should return 500 if there is a server error', async () => { + jest + .spyOn(orderRepository, 'findOne') + .mockRejectedValue(new Error('Database error')); + + const response = await request(app) + .put('/api/v1/order/1') + .send({ status: 'Failed' }); + + expect(response.status).toBe(500); + }); + }); +}); diff --git a/src/__test__/payment.test.ts b/src/__test__/payment.test.ts index 31c59875..254d6aa4 100644 --- a/src/__test__/payment.test.ts +++ b/src/__test__/payment.test.ts @@ -5,97 +5,117 @@ import { Order } from '../database/models/orderEntity'; import dbConnection from '../database'; import Stripe from 'stripe'; - jest.mock('stripe'); const MockedStripe = Stripe as jest.Mocked; - describe('handlePayment', () => { - let token: string; - let order: Order; - - - beforeAll(async () => { - await dbConnection.initialize(); - await dbConnection.synchronize(true); // This will drop all tables - token = await getBuyerToken(); - // Create a mock order in the database - const orderRepository = dbConnection.getRepository(Order); - order = orderRepository.create({ - totalAmount: 100, - status: 'Pending', - trackingNumber: '123456', - paid: false, - }); - await orderRepository.save(order); - }); - - - afterAll(async () => { - await dbConnection.close(); - }); - - - it('should process payment successfully', async () => { - const mockChargesCreate = jest.fn().mockResolvedValue({ - id: 'charge_id', - amount: 10000, - currency: 'usd', - } as Stripe.Charge); - - - MockedStripe.prototype.charges = { - create: mockChargesCreate, - } as unknown as Stripe.ChargesResource; - - - const response = await request(app) - .post('/api/v1/buyer/payment') - .set('Authorization', `Bearer ${token}`) - .send({ token: 'fake-token', orderId: order.id }); - - - expect(response.status).toBe(200); - expect(response.body.success).toBe(true); - expect(response.body.paid).toBe(true); - expect(response.body.charge.id).toBe('charge_id'); - expect(mockChargesCreate).toHaveBeenCalledWith({ - amount: 10000, - currency: 'usd', - description: 'Test Charge', - source: 'fake-token', - }); - }); - - - it('should return 404 if order not found', async () => { - const response = await request(app) - .post('/api/v1/buyer/payment') - .set('Authorization', `Bearer ${token}`) - .send({ token: 'fake-token', orderId: 999 }); - - - expect(response.status).toBe(404); - expect(response.body.success).toBe(false); - expect(response.body.message).toBe('Order not found'); - }); - - - it('should return 400 if order already paid', async () => { - // Set the order as paid - const orderRepository = dbConnection.getRepository(Order); - order.paid = true; - await orderRepository.save(order); - - - const response = await request(app) - .post('/api/v1/buyer/payment') - .set('Authorization', `Bearer ${token}`) - .send({ token: 'fake-token', orderId: order.id }); - - - expect(response.status).toBe(400); - expect(response.body.success).toBe(false); - expect(response.body.message).toBe('Order has already been paid'); - }); + let token: string; + let order: Order; + + beforeAll(async () => { + await dbConnection.initialize(); + await dbConnection.synchronize(true); // This will drop all tables + token = await getBuyerToken(); + // Create a mock order in the database + const orderRepository = dbConnection.getRepository(Order); + order = orderRepository.create({ + totalAmount: 100, + status: 'Pending', + trackingNumber: '123456', + paid: false, + }); + await orderRepository.save(order); + }); + + afterAll(async () => { + await dbConnection.close(); + }); + + it('should process payment successfully', async () => { + const mockChargesCreate = jest.fn().mockResolvedValue({ + id: 'charge_id', + amount: 10000, + currency: 'usd', + } as Stripe.Charge); + + MockedStripe.prototype.charges = { + create: mockChargesCreate, + } as unknown as Stripe.ChargesResource; + + const response = await request(app) + .post('/api/v1/buyer/payment') + .set('Authorization', `Bearer ${token}`) + .send({ token: 'fake-token', orderId: order.id }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.paid).toBe(true); + expect(response.body.charge.id).toBe('charge_id'); + expect(mockChargesCreate).toHaveBeenCalledWith({ + amount: 10000, + currency: 'usd', + description: 'Test Charge', + source: 'fake-token', + }); + }); + + it('should return 400 if payment is not successful', async () => { + // Set the order as not paid + const orderRepository = dbConnection.getRepository(Order); + order.paid = false; + await orderRepository.save(order); + + const mockChargesCreate = jest.fn().mockResolvedValue({ + id: 'charge_id', + amount: 10000, + currency: 'usd', + status: 'failed', + } as Stripe.Charge); + + MockedStripe.prototype.charges = { + create: mockChargesCreate, + } as unknown as Stripe.ChargesResource; + + const response = await request(app) + .post('/api/v1/buyer/payment') + .set('Authorization', `Bearer ${token}`) + .send({ token: 'fake-token', orderId: order.id }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + expect(response.body.paid).toBe(false); + expect(mockChargesCreate).toHaveBeenCalledWith({ + amount: 10000, + currency: 'usd', + description: 'Test Charge', + source: 'fake-token', + }); + }); + + it('should return 404 if order not found', async () => { + const response = await request(app) + .post('/api/v1/buyer/payment') + .set('Authorization', `Bearer ${token}`) + .send({ token: 'fake-token', orderId: 999 }); + + expect(response.status).toBe(404); + expect(response.body.success).toBe(false); + expect(response.body.message).toBe('Order not found'); + }); + + it('should return 400 if order already paid', async () => { + // Set the order as paid + const orderRepository = dbConnection.getRepository(Order); + order.paid = true; + await orderRepository.save(order); + + const response = await request(app) + .post('/api/v1/buyer/payment') + .set('Authorization', `Bearer ${token}`) + .send({ token: 'fake-token', orderId: order.id }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + expect(response.body.message).toBe('Order has already been paid'); + }); }); diff --git a/src/__test__/review.test.ts b/src/__test__/review.test.ts index d038ecc7..f9bc96f1 100644 --- a/src/__test__/review.test.ts +++ b/src/__test__/review.test.ts @@ -91,4 +91,13 @@ describe('Review controller test', () => { expect(responseReview.body.message).toEqual('you are already reviewed the product'); }) + + it('should return 400 for failed validation on create review', async () => { + const reviewBody = {content:'good', rating:15, productId:'some id'} + const responseReview = await request(app) + .post('/api/v1/review') + .set('Authorization', `Bearer ${buyerToken}`) + .send(reviewBody) + expect(responseReview.statusCode).toEqual(400); + }) }) \ No newline at end of file diff --git a/src/config/db.ts b/src/config/db.ts index 6082fa09..4a0acf56 100644 --- a/src/config/db.ts +++ b/src/config/db.ts @@ -47,4 +47,4 @@ const config: { production, }; -export default config[env]; \ No newline at end of file +export default config[env]; diff --git a/src/controller/buyerController.ts b/src/controller/buyerController.ts index f47a90c0..8a16f8bc 100644 --- a/src/controller/buyerController.ts +++ b/src/controller/buyerController.ts @@ -5,71 +5,69 @@ import errorHandler from '../middlewares/errorHandler'; import Stripe from 'stripe'; import { Order } from '../database/models/orderEntity'; - -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', { apiVersion: '2024-04-10' }); +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', { + apiVersion: '2024-04-10', +}); const productRepository = dbConnection.getRepository(Product); const orderRepository = dbConnection.getRepository(Order); - - export const getOneProduct = errorHandler( async (req: Request, res: Response) => { const productId = parseInt(req.params.id); - - + const product = await productRepository.findOne({ where: { id: productId }, relations: ['category'], }); - - + if (!product) { return res.status(404).json({ msg: 'Product not found' }); } - - + return res .status(200) .json({ msg: 'Product retrieved successfully', product }); } - ); - - - - - export const handlePayment = errorHandler( +); + +export const handlePayment = errorHandler( async (req: Request, res: Response) => { const { token, orderId } = req.body; - - + const order = await orderRepository.findOne({ where: { id: orderId } }); - - + if (!order) { - return res.status(404).json({ success: false, message: 'Order not found' }); + return res + .status(404) + .json({ success: false, message: 'Order not found' }); } - - + if (order.paid) { - return res.status(400).json({ success: false, message: 'Order has already been paid' }); + return res + .status(400) + .json({ success: false, message: 'Order has already been paid' }); } - - + const amountInCents = order.totalAmount * 100; - - + const charge = await stripe.charges.create({ amount: amountInCents, currency: 'usd', description: 'Test Charge', source: token, }); - - - order.paid = true; - await orderRepository.save(order); - - return res.status(200).json({ success: true, paid: true, charge}); + + if (charge.status === 'succeeded') { + order.paid = true; + await orderRepository.save(order); + + return res.status(200).json({ success: true, paid: true, charge }); + } else { + return res.status(400).json({ + success: false, + paid: false, + message: `Charge status: ${charge.status}`, + }); + } } - ); - \ No newline at end of file +); diff --git a/src/controller/buyerWishlistController.ts b/src/controller/buyerWishlistController.ts new file mode 100644 index 00000000..49b7389e --- /dev/null +++ b/src/controller/buyerWishlistController.ts @@ -0,0 +1,157 @@ +import { check, validationResult } from 'express-validator'; +import { Request, Response } from 'express'; +import BuyerWishList from '../database/models/buyerWishList'; +import UserModel from '../database/models/userModel'; +import Product from '../database/models/productEntity'; +import errorHandler from '../middlewares/errorHandler'; +import dbConnection from '../database'; + +const userRepository = dbConnection.getRepository(UserModel); +const productRepository = dbConnection.getRepository(Product); +const buyerWishListRepository = dbConnection.getRepository(BuyerWishList); + +const AddToWishListRules = [ + check('productId').isLength({ min: 1 }).withMessage('Product ID is required'), +]; + +export const AddItemInWishList = [ + ...AddToWishListRules, + errorHandler(async (req: Request, res: Response) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + const userId = req.user!.id; + const { productId, time } = req.body; + const wishListTime = time ? new Date(time) : new Date(); + + const user = await userRepository.findOne({ where: { id: userId } }); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + const product = await productRepository.findOne({ + where: { id: productId }, + }); + if (!product) { + return res.status(404).json({ message: 'Product not found' }); + } + + const existingWishListEntry = await buyerWishListRepository.findOne({ + where: { user: { id: userId } }, + relations: ['product'], + }); + + if (existingWishListEntry) { + const productExists = existingWishListEntry.product.some( + (p) => p.id === productId + ); + if (productExists) { + return res + .status(409) + .json({ message: 'Product is already in the wishlist' }); + } + existingWishListEntry.product.push(product); + existingWishListEntry.time = wishListTime; + const updatedWishList = await buyerWishListRepository.save( + existingWishListEntry + ); + return res.status(200).json({ + message: 'Product added to existing wishlist', + data: updatedWishList, + }); + } + + const newWishList = new BuyerWishList(); + newWishList.user = user; + newWishList.product = [product]; + newWishList.time = wishListTime; + + const savedWishList = await buyerWishListRepository.save(newWishList); + return res.status(201).json({ + message: 'Wishlist successfully created', + data: savedWishList, + }); + }), +]; + +const RemoveProductRules = [ + check('productId').isLength({ min: 1 }).withMessage('Product ID is required'), +]; + +export const RemoveProductFromWishList = [ + ...RemoveProductRules, + errorHandler(async (req: Request, res: Response) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const userId = req.user?.id; + const { productId } = req.body; + + const wishList = await buyerWishListRepository.findOne({ + where: { user: { id: userId } }, + relations: ['product'], + }); + + if (!wishList) { + return res.status(404).json({ message: 'Wishlist not found' }); + } + + const productIndex = wishList.product.findIndex((p) => p.id === productId); + if (productIndex === -1) { + return res.status(404).json({ message: 'Product not found in wishlist' }); + } + + wishList.product.splice(productIndex, 1); + await buyerWishListRepository.save(wishList); + + return res.status(200).json({ + message: 'Product successfully removed from wishlist', + data: wishList, + }); + }), +]; + +export const getAllWishList = errorHandler( + async (req: Request, res: Response) => { + const wishList = await buyerWishListRepository.find({ + select: { + product: true, + time: true, + user: { + lastName: true, + isVerified: true, + picture: true, + userType: { + name: true, + }, + }, + }, + relations: ['user', 'product'], + }); + + return res + .status(200) + .json({ message: 'Data retrieved successfully', data: wishList }); + } +); + +export const getOneWishList = errorHandler( + async (req: Request, res: Response) => { + const userId = req.user?.id; + if (!userId) { + return res.status(404).json({ message: 'User ID not found' }); + } + + const wishList = await buyerWishListRepository.findOne({ + where: { user: { id: userId } }, + relations: ['product'], + }); + + return res + .status(200) + .json({ message: 'Data retrieved successfully', data: wishList }); + } +); diff --git a/src/controller/orderController.ts b/src/controller/orderController.ts new file mode 100644 index 00000000..82e60027 --- /dev/null +++ b/src/controller/orderController.ts @@ -0,0 +1,49 @@ +import { Request, Response } from 'express'; +import dbConnection from '../database'; +import errorHandler from '../middlewares/errorHandler'; +import { Order } from '../database/models/orderEntity'; + +const orderRepository = dbConnection.getRepository(Order); + +export const updateOrderStatus = errorHandler( + async (req: Request, res: Response) => { + const { orderId } = req.params; + const { status } = req.body; + + const numericOrderId = parseInt(orderId, 10); + if (isNaN(numericOrderId)) { + return res.status(400).json({ msg: 'Invalid orderId' }); + } + + const validStatuses = [ + 'Pending', + 'Failed', + 'Canceled', + 'Paid', + 'Shipping', + 'Delivered', + 'Returned', + 'Completed', + ]; + if (!validStatuses.includes(status)) { + return res.status(400).json({ msg: 'Invalid status' }); + } + + const order = await orderRepository.findOne({ + where: { + id: numericOrderId, + }, + }); + + if (!order) { + return res.status(404).json({ msg: 'Order Not Found' }); + } + + order.status = status; + await orderRepository.save(order); + + return res + .status(200) + .json({ msg: `Order status updated to ${order.status}` }); + } +); diff --git a/src/controller/reviewController.ts b/src/controller/reviewController.ts index 069b6c7a..8e07a232 100644 --- a/src/controller/reviewController.ts +++ b/src/controller/reviewController.ts @@ -4,13 +4,24 @@ import Product from '../database/models/productEntity'; import dbConnection from '../database'; import errorHandler from '../middlewares/errorHandler'; import UserModel from '../database/models/userModel'; +import {createReviewSchema} from '../middlewares/createReviewSchema'; const productRepository = dbConnection.getRepository(Product); const reviewRepository = dbConnection.getRepository(Review); const userRepository = dbConnection.getRepository(UserModel); export const createReview = errorHandler(async (req: Request, res: Response) => { - const { content, rating, productId } = req.body; + const formData = req.body; + + const validationResult = createReviewSchema.validate(formData); + if (validationResult.error) { + return res + .status(400) + .json({ msg: validationResult.error.details[0].message }); + } + + const {content, rating, productId} = formData + const userId = req.user!.id; const product = await productRepository.findOne({ diff --git a/src/database/models/buyerWishList.ts b/src/database/models/buyerWishList.ts new file mode 100644 index 00000000..f7aaaf47 --- /dev/null +++ b/src/database/models/buyerWishList.ts @@ -0,0 +1,26 @@ +import { Column, Entity, ManyToMany, PrimaryGeneratedColumn, OneToOne, JoinColumn, JoinTable, ManyToOne } from 'typeorm'; +import Product from './productEntity'; +import UserModel from './userModel'; +import Category from './categoryEntity'; + +@Entity() +export default class BuyerWishList { + + @PrimaryGeneratedColumn() + id: number; + + @OneToOne(() => UserModel) + @JoinColumn() + user: UserModel; + + @ManyToMany(() => Product) + @JoinTable() + product: Product[]; + + @ManyToOne(() => Category) + category: Category; + + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + time: Date; + +} diff --git a/src/database/models/orderEntity.ts b/src/database/models/orderEntity.ts index bd2962a7..8b0a0b59 100644 --- a/src/database/models/orderEntity.ts +++ b/src/database/models/orderEntity.ts @@ -39,10 +39,12 @@ export class Order { @UpdateDateColumn() updatedAt: Date; - @OneToMany(() => OrderDetails, orderDetails => orderDetails.order, { cascade: true }) + @OneToMany(() => OrderDetails, (orderDetails) => orderDetails.order, { + cascade: true, + nullable: true, + }) orderDetails: OrderDetails[]; - + @Column({ type: 'boolean', default: false, nullable: true }) paid: boolean | null; - } diff --git a/src/docs/buyerDocs.ts b/src/docs/buyerDocs.ts index 3c8aa4b2..c2716a3a 100644 --- a/src/docs/buyerDocs.ts +++ b/src/docs/buyerDocs.ts @@ -1,20 +1,136 @@ /** * @swagger - * /api/v1/buyer/get_product/{id}: + * tags: + * name: buyer + * description: Category management + */ + +/** + * @openapi + * /buyer/addItemToWishList: + * post: + * tags: [buyer] + * @security bearerAuth + * summary: Adds an item to the wishlist + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * userId: + * type: integer + * productId: + * type: integer + * time: + * type: string + * categoryId: + * type: integer + * responses: + * 201: + * description: Wishlist item added successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/BuyerWishList' + */ + +/** + * @openapi + * /buyer/removeToWishList: + * delete: + * tags: [buyer] + * @security bearerAuth + * summary: Removes a product from the wishlist + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * userId: + * type: integer + * productId: + * type: integer + * responses: + * 200: + * description: Product successfully removed from wishlist + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/BuyerWishList' + */ + +/** + * @openapi + * /buyer/getWishList: * get: + * tags: [buyer] + * @security bearerAuth + * summary: Retrieves all wishlists + * responses: + * 200: + * description: Data retrieved successfully + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/BuyerWishList' + */ + +/** + * @swagger + * /buyer/addItemToWishList: + * post: * summary: Get a specific product * tags: [Buyer] * security: * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * type: string - * required: true - * description: ID of the product to get + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * productId: + * type: integer + * time: + * type: string * responses: * '200': - * description: Successful + * description: Wishlist item added successfully + * '400': + * description: Bad request + * '404': + * description: Wishlist not found + * '500': + * description: Internal server error + */ + +/** + * @swagger + * /api/v1/buyer/removeToWishList: + * delete: + * summary: Remove a product to WishList + * tags: [Buyer] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * productId: + * type: integer + * responses: + * '200': + * description: Successful Removed a Product * '404': * description: Product not found * '500': @@ -22,51 +138,35 @@ */ /** -* @swagger -* /api/v1/buyer/payment: -* post: -* summary: Create a charge -* tags: [Buyer] -* security: -* - bearerAuth: [] -* requestBody: -* content: -* application/json: -* schema: -* type: object -* properties: -* token: -* type: string -* description: Stripe token -* orderId: -* type: number -* description: Order ID -* required: -* - token -* - orderId -* responses: -* '200': -* description: Successful operation -* content: -* application/json: -* schema: -* type: object -* properties: -* success: -* type: boolean -* description: Whether the charge was successful -* charge: -* type: object -* description: The charge object returned by Stripe -* required: -* - success -* - charge -* '400': -* description: Invalid input or order has already been paid -* '404': -* description: Order not found -* '500': -* description: Internal Server Error -*/ - + * @swagger + * /api/v1/buyer/getWishList: + * get: + * summary: Get a All Wish List + * tags: [Buyer] + * security: + * - bearerAuth: [] + * responses: + * '200': + * description: Successful + * '404': + * description: Product not found + * '500': + * description: Internal Server Error + */ +/** + * @swagger + * /api/v1/buyer/getOneWishList: + * get: + * summary: Get One Wish List + * tags: [Buyer] + * security: + * - bearerAuth: [] + * responses: + * '200': + * description: Successful + * '404': + * description: Product not found + * '500': + * description: Internal Server Error + */ diff --git a/src/docs/buyerWishlist.ts b/src/docs/buyerWishlist.ts new file mode 100644 index 00000000..45b8520a --- /dev/null +++ b/src/docs/buyerWishlist.ts @@ -0,0 +1,220 @@ +/** + * @swagger + * tags: + * name: buyer + * description: Category management + */ + +/** + * @openapi + * /buyer/addItemToWishList: + * post: + * tags: [buyer] + * @security bearerAuth + * summary: Adds an item to the wishlist + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * userId: + * type: integer + * productId: + * type: integer + * time: + * type: string + * categoryId: + * type: integer + * responses: + * 201: + * description: Wishlist item added successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/BuyerWishList' + */ + +/** + * @openapi + * /buyer/removeToWishList: + * delete: + * tags: [buyer] + * @security bearerAuth + * summary: Removes a product from the wishlist + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * userId: + * type: integer + * productId: + * type: integer + * responses: + * 200: + * description: Product successfully removed from wishlist + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/BuyerWishList' + */ + +/** + * @openapi + * /buyer/getWishList: + * get: + * tags: [buyer] + * @security bearerAuth + * summary: Retrieves all wishlists + * responses: + * 200: + * description: Data retrieved successfully + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/BuyerWishList' + */ + +/** + * @swagger + * /buyer/addItemToWishList: + * post: + * summary: Get a specific product + * tags: [Buyer] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * productId: + * type: integer + * time: + * type: string + * responses: + * '200': + * description: Wishlist item added successfully + * '400': + * description: Bad request + * '404': + * description: Wishlist not found + * '500': + * description: Internal server error + */ + +/** + * @swagger + * /api/v1/buyer/removeToWishList: + * delete: + * summary: Remove a product to WishList + * tags: [Buyer] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * productId: + * type: integer + * responses: + * '200': + * description: Successful Removed a Product + * '404': + * description: Product not found + * '500': + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/buyer/getWishList: + * get: + * summary: Get a All Wish List + * tags: [Buyer] + * security: + * - bearerAuth: [] + * responses: + * '200': + * description: Successful + * '404': + * description: Product not found + * '500': + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/buyer/getOneWishList: + * get: + * summary: Get One Wish List + * tags: [Buyer] + * security: + * - bearerAuth: [] + * responses: + * '200': + * description: Successful + * '404': + * description: Product not found + * '500': + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/buyer/payment: + * post: + * summary: Create a charge + * tags: [Buyer] + * security: + * - bearerAuth: [] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * token: + * type: string + * description: Stripe token + * orderId: + * type: number + * description: Order ID + * required: + * - token + * - orderId + * responses: + * '200': + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Whether the charge was successful + * charge: + * type: object + * description: The charge object returned by Stripe + * required: + * - success + * - charge + * '400': + * description: Invalid input or order has already been paid + * '404': + * description: Order not found + * '500': + * description: Internal Server Error + */ diff --git a/src/docs/cartDocs.ts b/src/docs/cartDocs.ts index 131f0bfa..768603ee 100644 --- a/src/docs/cartDocs.ts +++ b/src/docs/cartDocs.ts @@ -224,3 +224,62 @@ * 500: * description: Internal Server Error */ + +/** + * @swagger + * /api/v1/order/{orderId}: + * put: + * summary: Update the status of an order + * tags: [Order] + * parameters: + * - name: orderId + * in: path + * required: true + * description: ID of the order to update + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * enum: [Pending, Failed, Canceled, Paid, Shipping, Delivered, Returned, Completed] + * example: Failed + * responses: + * 200: + * description: Order status updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * msg: + * type: string + * example: Order status updated to Failed + * 400: + * description: Invalid request + * content: + * application/json: + * schema: + * type: object + * properties: + * msg: + * type: string + * example: Invalid status + * 404: + * description: Order not found + * content: + * application/json: + * schema: + * type: object + * properties: + * msg: + * type: string + * example: Order Not Found + * 500: + * description: Internal Server Error + */ diff --git a/src/docs/reviewDocs.ts b/src/docs/reviewDocs.ts index e06e2299..2fdd6d73 100644 --- a/src/docs/reviewDocs.ts +++ b/src/docs/reviewDocs.ts @@ -4,31 +4,21 @@ * post: * summary: Create a new review * tags: [Reviews] - * consumes: - * - application/json - * produces: - * - application/json - * parameters: - * - in: body - * name: review - * description: The review object - * required: true - * schema: - * type: object - * properties: - * content: - * type: string - * description: The content of the review - * rating: - * type: integer - * description: The rating of the review (1 to 5) - * productId: - * type: integer - * description: The ID of the product being reviewed - * example: - * content: "This product is amazing!" - * rating: 5 - * productId: 50 + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * type: string + * rating: + * type: number + * productId: + * type: number * responses: * '201': * description: Review created successfully @@ -40,7 +30,7 @@ * description: A success message * review: * '400': - * description: Bad request, check the request body + * description: Bad request, failed validation * '404': * description: Product not found * '409': diff --git a/src/middlewares/createReviewSchema.ts b/src/middlewares/createReviewSchema.ts new file mode 100644 index 00000000..3eba04df --- /dev/null +++ b/src/middlewares/createReviewSchema.ts @@ -0,0 +1,8 @@ +import Joi from 'joi'; + + +export const createReviewSchema = Joi.object({ + content: Joi.string().required(), + rating: Joi.number().min(0).max(5).required(), + productId: Joi.number().required() +}) \ No newline at end of file diff --git a/src/routes/buyerRoutes.ts b/src/routes/buyerRoutes.ts index 4159a39f..0952c5af 100644 --- a/src/routes/buyerRoutes.ts +++ b/src/routes/buyerRoutes.ts @@ -2,17 +2,25 @@ import { Router } from 'express'; import { checkRole } from '../middlewares/authorize'; import { getOneProduct } from '../controller/buyerController'; import { IsLoggedIn } from '../middlewares/isLoggedIn'; +import { + AddItemInWishList, + RemoveProductFromWishList, + getAllWishList, + getOneWishList, +} from '../controller/buyerWishlistController'; import { handlePayment } from '../controller/buyerController'; const buyerRouter = Router(); buyerRouter.use(IsLoggedIn, checkRole(['Buyer'])); - buyerRouter.get('/get_product/:id', getOneProduct); +buyerRouter.post('/addItemToWishList', IsLoggedIn, AddItemInWishList); +buyerRouter.delete('/removeToWishList', IsLoggedIn, RemoveProductFromWishList); +buyerRouter.get('/getWishList', IsLoggedIn, getAllWishList); +buyerRouter.get('/getOneWishList', IsLoggedIn, getOneWishList); buyerRouter.post('/payment', handlePayment); - export default buyerRouter; diff --git a/src/routes/index.ts b/src/routes/index.ts index b2fb9092..97cfe421 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -8,6 +8,7 @@ import cartRoutes from '../routes/cartRoutes'; import couponRouter from './couponRoute'; import chekoutRoutes from './checkoutRoutes'; import reviewRoute from './reviewRoutes'; +import orderRoutes from './orderRoutes'; const router = Router(); @@ -19,6 +20,7 @@ router.use('/buyer', buyerRoutes); router.use('/cart', cartRoutes); router.use('/coupons', couponRouter); router.use('/checkout', chekoutRoutes); -router.use('/review',reviewRoute) +router.use('/review', reviewRoute); +router.use('/order', orderRoutes); export default router; diff --git a/src/routes/orderRoutes.ts b/src/routes/orderRoutes.ts new file mode 100644 index 00000000..5cd5acba --- /dev/null +++ b/src/routes/orderRoutes.ts @@ -0,0 +1,9 @@ +import { Router } from 'express'; + +import { updateOrderStatus } from '../controller/orderController'; + +const orderRoutes = Router(); + +orderRoutes.route('/:orderId').put(updateOrderStatus); + +export default orderRoutes;