From aa53a7caaeb595bee2a3f5200e8a92b5e50d7cae Mon Sep 17 00:00:00 2001 From: 13XAVI Date: Wed, 29 May 2024 20:49:26 +0200 Subject: [PATCH 1/4] adde some file --- package.json | 2 +- src/__test__/buyerWishlist.test.ts | 123 ++++++++++++ src/controller/buyerController.ts | 58 +++--- src/controller/buyerWishlistController.ts | 154 +++++++++++++++ src/database/models/buyerWishList.ts | 26 +++ src/docs/buyerDocs.ts | 193 ++++++++++++++----- src/docs/buyerWishlist.ts | 220 ++++++++++++++++++++++ src/routes/buyerRoutes.ts | 12 +- 8 files changed, 704 insertions(+), 84 deletions(-) create mode 100644 src/__test__/buyerWishlist.test.ts create mode 100644 src/controller/buyerWishlistController.ts create mode 100644 src/database/models/buyerWishList.ts create mode 100644 src/docs/buyerWishlist.ts diff --git a/package.json b/package.json index 3f3ff67..093c457 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 0000000..f8a2f3d --- /dev/null +++ b/src/__test__/buyerWishlist.test.ts @@ -0,0 +1,123 @@ +import request from 'supertest'; +import app from '../app'; +import { afterAllHook, beforeAllHook } from './testSetup'; +import { + getBuyerToken, + getVendorToken, + } from './testSetup'; + +beforeAll(beforeAllHook); +afterAll(afterAllHook); +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'); + }); + }); + + 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'); + }); + }) +}) diff --git a/src/controller/buyerController.ts b/src/controller/buyerController.ts index f47a90c..789f623 100644 --- a/src/controller/buyerController.ts +++ b/src/controller/buyerController.ts @@ -5,71 +5,61 @@ 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}); + + return res.status(200).json({ success: true, paid: true, charge }); } - ); - \ No newline at end of file +); diff --git a/src/controller/buyerWishlistController.ts b/src/controller/buyerWishlistController.ts new file mode 100644 index 0000000..1378ab2 --- /dev/null +++ b/src/controller/buyerWishlistController.ts @@ -0,0 +1,154 @@ +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 } }); + const product = await productRepository.findOne({ where: { id: productId } }); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + 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: 'Data 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/database/models/buyerWishList.ts b/src/database/models/buyerWishList.ts new file mode 100644 index 0000000..f7aaaf4 --- /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/docs/buyerDocs.ts b/src/docs/buyerDocs.ts index 3c8aa4b..16663d8 100644 --- a/src/docs/buyerDocs.ts +++ b/src/docs/buyerDocs.ts @@ -22,51 +22,150 @@ */ /** -* @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/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: + * '202': + * 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/momoPay: + * post: + * summary: Pay order using momo + * tags: [Buyer] + * security: + * - bearerAuth: [] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * momoNumber: + * type: string + * description: Mobile Money Number + * orderId: + * type: number + * description: Order ID + * required: + * - momoNumber + * - orderId + * responses: + * '200': + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: the charge was successful + * message: + * description: 'Transaction Accepted' + * requestId: + * type: string + * required: + * - success + * - message + * - requestId + * '400': + * description: Invalid input or order has already been paid + * '404': + * description: Order not found + * '500': + * description: Internal Server Error + */ +/** + * @swagger + * /api/v1/buyer/getPaymentStatus/{id}: + * get: + * summary: Get Payment Status + * tags: [Buyer] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * type: number + * required: true + * description: Order Id + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * requestId: + * type: string + * description: Request Id For Payment + * required: + * - momoNumber + * - orderId + * responses: + * '200': + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: the charge was successful + * message: 'Transaction Done Successfully' + * requestId: + * type: string + * required: + * - success + * - message + * - requestId + * '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/buyerWishlist.ts b/src/docs/buyerWishlist.ts new file mode 100644 index 0000000..45b8520 --- /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/routes/buyerRoutes.ts b/src/routes/buyerRoutes.ts index 4159a39..0952c5a 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; From ec2f7279582ff6da466911db4b502b8394dca582 Mon Sep 17 00:00:00 2001 From: 13XAVI Date: Thu, 30 May 2024 16:24:17 +0200 Subject: [PATCH 2/4] added coverages --- package-lock.json | 42 +++- src/__test__/buyerWishlist.test.ts | 257 ++++++++++++++++------ src/__test__/payment.test.ts | 165 +++++++------- src/controller/buyerWishlistController.ts | 155 ++++++------- src/docs/buyerDocs.ts | 233 ++++++++++---------- 5 files changed, 493 insertions(+), 359 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16615fa..aa3d203 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/src/__test__/buyerWishlist.test.ts b/src/__test__/buyerWishlist.test.ts index f8a2f3d..8a7f4ff 100644 --- a/src/__test__/buyerWishlist.test.ts +++ b/src/__test__/buyerWishlist.test.ts @@ -1,10 +1,7 @@ import request from 'supertest'; import app from '../app'; import { afterAllHook, beforeAllHook } from './testSetup'; -import { - getBuyerToken, - getVendorToken, - } from './testSetup'; +import { getBuyerToken, getVendorToken } from './testSetup'; beforeAll(beforeAllHook); afterAll(afterAllHook); @@ -14,110 +11,226 @@ let productId: number; let categoryId: number; beforeAll(async () => { - buyerToken = await getBuyerToken(); - vendorToken = await getVendorToken(); + buyerToken = await getBuyerToken(); + vendorToken = await getVendorToken(); - const categoryData = { - name: 'Category4', - description: 'category description', + 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'); + 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; }); - + +describe('Buyer Wishlist Operations', () => { 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'); + }); - it('should add an item to the wishlist', async () => { + it('should return 404 if the product does not exist', async () => { + const res = await request(app) + .post('/api/v1/buyer/addItemToWishList') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: 9999, + time: '2024-05-21T12:00:00Z', + }); - + expect(res.statusCode).toEqual(404); + expect(res.body.message).toContain('Product not found'); + }); + it('should return 404 if the user does not exist', async () => { + const invalidBuyerToken = 'invalidBuyerToken'; const res = await request(app) - .post('/api/v1/buyer/addItemToWishList') - .set('Authorization', `Bearer ${buyerToken}`) - .send({ + .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'); + expect(res.statusCode).toEqual(404); + expect(res.body.message).toContain('User not found'); }); }); 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({ + .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'); + expect(res.body.message).toContain( + 'Product successfully removed from wishlist' + ); + }); + + it('should return 404 if the product is not in the wishlist', async () => { + const res = await request(app) + .delete('/api/v1/buyer/removeToWishList') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: 9999, // non-existing product + }); + + expect(res.statusCode).toEqual(404); + expect(res.body.message).toContain('Product not found in wishlist'); + }); + + it('should return 404 if the wishlist does not exist', async () => { + const invalidBuyerToken = 'invalidBuyerToken'; + const res = await request(app) + .delete('/api/v1/buyer/removeToWishList') + .set('Authorization', `Bearer ${invalidBuyerToken}`) + .send({ + productId: productId, + }); + + expect(res.statusCode).toEqual(404); + expect(res.body.message).toContain('Wishlist not found'); }); }); 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}`); + .get('/api/v1/buyer/getWishList') + .set('Authorization', `Bearer ${buyerToken}`); + + expect(res.statusCode).toEqual(200); + expect(res.body.message).toContain('Data retrieved successfully'); + }); + + it('should return 401 if the token is invalid', async () => { + const invalidToken = 'invalidToken'; + const res = await request(app) + .get('/api/v1/buyer/getWishList') + .set('Authorization', `Bearer ${invalidToken}`); + + expect(res.statusCode).toEqual(401); + expect(res.body.message).toContain('Unauthorized: Invalid token'); + }); + }); + + describe('GET /api/v1/buyer/getOneWishList', () => { + it('should get a specific wishlist item', 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('GET /api/v1/buyer/getOneWishList', () => { - it('should get all wishlists', async () => { - const res = await request(app) + it('should return 401 if the token is invalid', async () => { + const invalidToken = 'invalidToken'; + const res = await request(app) + .get('/api/v1/buyer/getOneWishList') + .set('Authorization', `Bearer ${invalidToken}`); + + expect(res.statusCode).toEqual(401); + expect(res.body.message).toContain('Unauthorized: Invalid token'); + }); + + it('should return 404 if the wishlist does not exist', async () => { + const invalidBuyerToken = 'invalidBuyerToken'; + 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'); - }); - }) -}) + .set('Authorization', `Bearer ${invalidBuyerToken}`); + + expect(res.statusCode).toEqual(404); + expect(res.body.message).toContain('Wishlist not found'); + }); + }); + + 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, // non-existing product + }); + + expect(res.statusCode).toEqual(404); + expect(res.body.message).toContain('Product not found in wishlist'); + }); + }); + + describe('Removing a product from the wishlist', () => { + it('should respond with 404 if the product is not in the wishlist', async () => { + const nonExistingProductId = 'nonexistentProductId'; + const res = await request(app) + .delete('/api/v1/buyer/removeToWishList') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: nonExistingProductId, + }); + + expect(res.statusCode).toEqual(404); + expect(res.body.message).toContain('Product not found in wishlist'); + }); + }); +}); diff --git a/src/__test__/payment.test.ts b/src/__test__/payment.test.ts index 31c5987..d0d032c 100644 --- a/src/__test__/payment.test.ts +++ b/src/__test__/payment.test.ts @@ -5,97 +5,84 @@ 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 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/controller/buyerWishlistController.ts b/src/controller/buyerWishlistController.ts index 1378ab2..49b7389 100644 --- a/src/controller/buyerWishlistController.ts +++ b/src/controller/buyerWishlistController.ts @@ -10,9 +10,7 @@ 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'), ]; @@ -23,17 +21,18 @@ export const AddItemInWishList = [ if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } - const userId = req.user!.id + 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 } }); - const product = await productRepository.findOne({ where: { id: productId } }); - 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' }); } @@ -44,14 +43,23 @@ export const AddItemInWishList = [ }); if (existingWishListEntry) { - const productExists = existingWishListEntry.product.some(p => p.id === productId); + const productExists = existingWishListEntry.product.some( + (p) => p.id === productId + ); if (productExists) { - return res.status(409).json({ message: 'Product is already in the wishlist' }); + 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 updatedWishList = await buyerWishListRepository.save( + existingWishListEntry + ); + return res.status(200).json({ + message: 'Product added to existing wishlist', + data: updatedWishList, + }); } const newWishList = new BuyerWishList(); @@ -60,7 +68,6 @@ export const AddItemInWishList = [ newWishList.time = wishListTime; const savedWishList = await buyerWishListRepository.save(newWishList); - return res.status(201).json({ message: 'Wishlist successfully created', data: savedWishList, @@ -68,82 +75,77 @@ export const AddItemInWishList = [ }), ]; -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' }); - } - +const RemoveProductRules = [ + check('productId').isLength({ min: 1 }).withMessage('Product ID is required'), +]; - wishList.product.splice(productIndex, 1); - await buyerWishListRepository.save(wishList); - - return res.status(200).json({ - message: 'Product successfully removed from wishlist', - data: wishList, - }); - }), - ]; - +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)=>{ +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, - } - } - + 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: 'Data Id Not Found'}); - } - const wishList = await buyerWishListRepository.findOne({ + } +); + +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'], }); @@ -151,4 +153,5 @@ const removeProductRules = [ return res .status(200) .json({ message: 'Data retrieved successfully', data: wishList }); - }) + } +); diff --git a/src/docs/buyerDocs.ts b/src/docs/buyerDocs.ts index 16663d8..c2716a3 100644 --- a/src/docs/buyerDocs.ts +++ b/src/docs/buyerDocs.ts @@ -1,171 +1,172 @@ /** * @swagger - * /api/v1/buyer/get_product/{id}: - * get: - * 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 - * responses: - * '200': - * description: Successful - * '404': - * description: Product not found - * '500': - * description: Internal Server Error + * tags: + * name: buyer + * description: Category management */ /** - * @swagger - * /api/v1/buyer/payment: + * @openapi + * /buyer/addItemToWishList: * post: - * summary: Create a charge - * tags: [Buyer] - * security: - * - bearerAuth: [] + * tags: [buyer] + * @security bearerAuth + * summary: Adds an item to the wishlist * requestBody: + * required: true * content: * application/json: * schema: * type: object * properties: - * token: + * userId: + * type: integer + * productId: + * type: integer + * time: * type: string - * description: Stripe token - * orderId: - * type: number - * description: Order ID - * required: - * - token - * - orderId + * categoryId: + * type: integer * responses: - * '202': - * description: Successful operation + * 201: + * description: Wishlist item added successfully * 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 + * $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 - * /api/v1/buyer/momoPay: + * /buyer/addItemToWishList: * post: - * summary: Pay order using momo + * summary: Get a specific product * tags: [Buyer] * security: * - bearerAuth: [] * requestBody: + * required: true * content: * application/json: * schema: * type: object * properties: - * momoNumber: + * productId: + * type: integer + * time: * type: string - * description: Mobile Money Number - * orderId: - * type: number - * description: Order ID - * required: - * - momoNumber - * - orderId * responses: * '200': - * description: Successful operation - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: the charge was successful - * message: - * description: 'Transaction Accepted' - * requestId: - * type: string - * required: - * - success - * - message - * - requestId + * description: Wishlist item added successfully * '400': - * description: Invalid input or order has already been paid + * description: Bad request * '404': - * description: Order not found + * description: Wishlist not found * '500': - * description: Internal Server Error + * description: Internal server error */ /** * @swagger - * /api/v1/buyer/getPaymentStatus/{id}: - * get: - * summary: Get Payment Status + * /api/v1/buyer/removeToWishList: + * delete: + * summary: Remove a product to WishList * tags: [Buyer] * security: * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * type: number - * required: true - * description: Order Id * requestBody: + * required: true * content: * application/json: * schema: * type: object * properties: - * requestId: - * type: string - * description: Request Id For Payment - * required: - * - momoNumber - * - orderId + * productId: + * type: integer * responses: * '200': - * description: Successful operation - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: the charge was successful - * message: 'Transaction Done Successfully' - * requestId: - * type: string - * required: - * - success - * - message - * - requestId - * '400': - * description: Invalid input or order has already been paid + * 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: Order not found + * 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 */ From 81f80fa7848d84b5ab0d92074c456bc7bfcdcd90 Mon Sep 17 00:00:00 2001 From: 13XAVI Date: Thu, 30 May 2024 17:40:38 +0200 Subject: [PATCH 3/4] adde some file --- src/__test__/buyerWishlist.test.ts | 281 +++++++++++------------------ 1 file changed, 108 insertions(+), 173 deletions(-) diff --git a/src/__test__/buyerWishlist.test.ts b/src/__test__/buyerWishlist.test.ts index 8a7f4ff..f541597 100644 --- a/src/__test__/buyerWishlist.test.ts +++ b/src/__test__/buyerWishlist.test.ts @@ -47,190 +47,125 @@ beforeAll(async () => { .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('Buyer Wishlist Operations', () => { - 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'); - }); - - it('should return 404 if the product does not exist', async () => { - const res = await request(app) - .post('/api/v1/buyer/addItemToWishList') - .set('Authorization', `Bearer ${buyerToken}`) - .send({ - productId: 9999, - time: '2024-05-21T12:00:00Z', - }); - - expect(res.statusCode).toEqual(404); - expect(res.body.message).toContain('Product not found'); - }); - - it('should return 404 if the user does not exist', async () => { - const invalidBuyerToken = 'invalidBuyerToken'; - 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(404); - expect(res.body.message).toContain('User not found'); - }); +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'); }); - 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' - ); - }); - - it('should return 404 if the product is not in the wishlist', async () => { - const res = await request(app) - .delete('/api/v1/buyer/removeToWishList') - .set('Authorization', `Bearer ${buyerToken}`) - .send({ - productId: 9999, // non-existing product - }); - - expect(res.statusCode).toEqual(404); - expect(res.body.message).toContain('Product not found in wishlist'); - }); - - it('should return 404 if the wishlist does not exist', async () => { - const invalidBuyerToken = 'invalidBuyerToken'; - const res = await request(app) - .delete('/api/v1/buyer/removeToWishList') - .set('Authorization', `Bearer ${invalidBuyerToken}`) - .send({ - productId: productId, - }); - - expect(res.statusCode).toEqual(404); - expect(res.body.message).toContain('Wishlist not found'); - }); + 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('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'); - }); - - it('should return 401 if the token is invalid', async () => { - const invalidToken = 'invalidToken'; - const res = await request(app) - .get('/api/v1/buyer/getWishList') - .set('Authorization', `Bearer ${invalidToken}`); - - expect(res.statusCode).toEqual(401); - expect(res.body.message).toContain('Unauthorized: Invalid token'); - }); +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/getOneWishList', () => { - it('should get a specific wishlist item', 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'); - }); - - it('should return 401 if the token is invalid', async () => { - const invalidToken = 'invalidToken'; - const res = await request(app) - .get('/api/v1/buyer/getOneWishList') - .set('Authorization', `Bearer ${invalidToken}`); - - expect(res.statusCode).toEqual(401); - expect(res.body.message).toContain('Unauthorized: Invalid token'); - }); - - it('should return 404 if the wishlist does not exist', async () => { - const invalidBuyerToken = 'invalidBuyerToken'; - const res = await request(app) - .get('/api/v1/buyer/getOneWishList') - .set('Authorization', `Bearer ${invalidBuyerToken}`); - - expect(res.statusCode).toEqual(404); - expect(res.body.message).toContain('Wishlist not found'); - }); +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'); + }); +}); + +describe('GET /api/v1/buyer/getOneWishList', () => { + const invalidToken = 'invalid Token'; + it('should return an error when the token is invalid', async () => { + const res = await request(app) + .get('/api/v1/buyer/getOneWishList') + .set('Authorization', `Bearer ${invalidToken}`); - 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, // non-existing product - }); - - expect(res.statusCode).toEqual(404); - expect(res.body.message).toContain('Product not found in wishlist'); - }); + expect(res.statusCode).toEqual(401); + expect(res.body.message).toContain('Unauthorized: Invalid token'); }); +}); - describe('Removing a product from the wishlist', () => { - it('should respond with 404 if the product is not in the wishlist', async () => { - const nonExistingProductId = 'nonexistentProductId'; - const res = await request(app) - .delete('/api/v1/buyer/removeToWishList') - .set('Authorization', `Bearer ${buyerToken}`) - .send({ - productId: nonExistingProductId, - }); - - expect(res.statusCode).toEqual(404); - expect(res.body.message).toContain('Product not found in wishlist'); - }); +describe('Removing a product from the wishlist', () => { + it('should respond with 404 if the product is not in the wishlist', async () => { + const NonExistingProductId = 'nonexistentProductId'; + const res = await request(app) + .delete('/api/v1/buyer/removeToWishList') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: NonExistingProductId, + }); + + expect(res.statusCode).toEqual(404); + expect(res.body.message).toContain('Product not found in wishlist'); }); }); From d4a145cf931e64bd9285dfeea4d865741b2cda92 Mon Sep 17 00:00:00 2001 From: 13XAVI Date: Fri, 31 May 2024 09:49:17 +0200 Subject: [PATCH 4/4] mtn payment files --- package-lock.json | 195 +++++++-- package.json | 5 +- src/__test__/Momo.test.ts | 232 +++++++++++ src/__test__/coupon.test.ts | 657 +++++++++++++++--------------- src/controller/buyerController.ts | 200 ++++++++- src/docs/buyerDocs.ts | 211 +++++++--- src/docs/buyerWishlist.ts | 220 ---------- src/routes/buyerRoutes.ts | 14 +- 8 files changed, 1091 insertions(+), 643 deletions(-) create mode 100644 src/__test__/Momo.test.ts delete mode 100644 src/docs/buyerWishlist.ts diff --git a/package-lock.json b/package-lock.json index aa3d203..d0420a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@types/node-fetch": "^2.6.11", "@types/passport-google-oauth20": "^2.0.14", "axios": "^1.6.8", "bcrypt": "^5.1.1", @@ -22,11 +23,12 @@ "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", + "node-fetch": "^3.3.2", "nodemailer": "^6.9.13", "nodemon": "^3.1.0", "otplib": "^12.0.1", @@ -65,8 +67,9 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "prettier": "^3.2.5", - "ts-jest": "^29.1.4", + "ts-jest": "^29.1.2", "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" } @@ -1418,6 +1421,25 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1879,6 +1901,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/nodemailer": { "version": "6.4.15", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", @@ -3441,6 +3472,35 @@ "yarn": ">=1" } }, + "node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dev": true, + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4648,6 +4708,28 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4828,6 +4910,17 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/formidable": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz", @@ -6257,6 +6350,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -6667,11 +6770,6 @@ "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", @@ -7215,41 +7313,52 @@ "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", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dependencies": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": "4.x || >=6.0.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-fetch/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" } }, "node_modules/node-int64": { @@ -8206,6 +8315,12 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "dev": true + }, "node_modules/promisify-call": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/promisify-call/-/promisify-call-2.0.4.tgz", @@ -8230,14 +8345,6 @@ "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", @@ -10134,6 +10241,14 @@ "makeerror": "1.0.12" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 093c457..810eea5 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" @@ -24,6 +24,7 @@ }, "homepage": "https://github.com/atlp-rwanda/dynamites-ecomm-be#readme", "dependencies": { + "@types/node-fetch": "^2.6.11", "@types/passport-google-oauth20": "^2.0.14", "axios": "^1.6.8", "bcrypt": "^5.1.1", @@ -42,6 +43,7 @@ "jsonwebtoken": "^9.0.2", "mailgun-js": "^0.22.0", "morgan": "^1.10.0", + "node-fetch": "^3.3.2", "nodemailer": "^6.9.13", "nodemon": "^3.1.0", "otplib": "^12.0.1", @@ -99,6 +101,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "prettier": "^3.2.5", "ts-jest": "^29.1.2", "ts-node-dev": "^2.0.0", diff --git a/src/__test__/Momo.test.ts b/src/__test__/Momo.test.ts new file mode 100644 index 0000000..ced21b8 --- /dev/null +++ b/src/__test__/Momo.test.ts @@ -0,0 +1,232 @@ +import request from 'supertest'; +import express from 'express'; +import bodyParser from 'body-parser'; +import { + MomohandlePayment, + checkPaymentStatus, + orderRepository, +} from '../controller/buyerController'; +import { Order } from '../database/models/orderEntity'; + +jest.mock('../controller/buyerController'); +jest.mock('node-fetch', () => require('jest-fetch-mock')); + +const fetch = require('node-fetch'); +const { Response } = jest.requireActual('node-fetch'); + +// Create an instance of your Express app +const app = express(); +app.use(bodyParser.json()); +app.post('/api/v1/buyer/payment', MomohandlePayment); +app.post('/api/v1/buyer/getPaymentStatus/:id', checkPaymentStatus); + +describe('Buyer Payment Endpoints', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('POST /api/v1/buyer/payment', () => { + it('should handle a successful payment', async () => { + const token = 'testToken'; + const order = { id: 1, totalAmount: 100, paid: false }; + const validateResponse = { result: true }; + const paymentResponse = new Response(JSON.stringify({}), { status: 202 }); + + (orderRepository.findOne as jest.Mock).mockResolvedValue(order); + + (fetch as jest.MockedFunction) + .mockResolvedValueOnce( + new Response(JSON.stringify({ access_token: token }), { status: 200 }) + ) + .mockResolvedValueOnce( + new Response(JSON.stringify(validateResponse), { status: 200 }) + ) + .mockResolvedValueOnce(paymentResponse); + + const response = await request(app) + .post('/api/v1/buyer/payment') + .send({ orderId: 1, momoNumber: '1234567890' }); + + expect(response.status).toBe(202); + expect(response.body).toEqual({ + message: 'Transaction Accepted', + requestId: expect.any(String), + }); + }); + + it('should return 400 if Momo number does not exist', async () => { + const token = 'testToken'; + const validateResponse = { result: false }; + + (fetch as jest.MockedFunction) + .mockResolvedValueOnce( + new Response(JSON.stringify({ access_token: token }), { status: 200 }) + ) + .mockResolvedValueOnce( + new Response(JSON.stringify(validateResponse), { status: 200 }) + ); + + const response = await request(app) + .post('/api/v1/buyer/payment') + .send({ orderId: 1, momoNumber: 'invalidNumber' }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ + message: 'Your Momo Number does not Exist', + }); + }); + + it('should return 404 if order is not found', async () => { + const token = 'testToken'; + const validateResponse = { result: true }; + + (orderRepository.findOne as jest.Mock).mockResolvedValue(null); + + (fetch as jest.MockedFunction) + .mockResolvedValueOnce( + new Response(JSON.stringify({ access_token: token }), { status: 200 }) + ) + .mockResolvedValueOnce( + new Response(JSON.stringify(validateResponse), { status: 200 }) + ); + + const response = await request(app) + .post('/api/v1/buyer/payment') + .send({ orderId: 999, momoNumber: '1234567890' }); + + expect(response.status).toBe(404); + expect(response.body).toEqual({ + success: false, + message: 'Order not found', + }); + }); + + it('should return 400 if order is already paid', async () => { + const token = 'testToken'; + const order = { id: 1, totalAmount: 100, paid: true }; + const validateResponse = { result: true }; + + (orderRepository.findOne as jest.Mock).mockResolvedValue(order); + + (fetch as jest.MockedFunction) + .mockResolvedValueOnce( + new Response(JSON.stringify({ access_token: token }), { status: 200 }) + ) + .mockResolvedValueOnce( + new Response(JSON.stringify(validateResponse), { status: 200 }) + ); + + const response = await request(app) + .post('/api/v1/buyer/payment') + .send({ orderId: 1, momoNumber: '1234567890' }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ + success: false, + message: 'Order has already been paid', + }); + }); + + it('should return 400 if payment request fails', async () => { + const token = 'testToken'; + const order = { id: 1, totalAmount: 100, paid: false }; + const validateResponse = { result: true }; + + (orderRepository.findOne as jest.Mock).mockResolvedValue(order); + + (fetch as jest.MockedFunction) + .mockResolvedValueOnce( + new Response(JSON.stringify({ access_token: token }), { status: 200 }) + ) + .mockResolvedValueOnce( + new Response(JSON.stringify(validateResponse), { status: 200 }) + ) + .mockResolvedValueOnce(new Response(null, { status: 400 })); + + const response = await request(app) + .post('/api/v1/buyer/payment') + .send({ orderId: 1, momoNumber: '1234567890' }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ message: 'Transaction Fail' }); + }); + }); + + describe('POST /api/v1/buyer/getPaymentStatus/:id', () => { + it('should handle checking payment status successfully', async () => { + const token = 'testToken'; + const order = { id: 44, paid: false }; + const statusResponse = { status: 'SUCCESSFUL', reason: null }; + + (orderRepository.findOne as jest.Mock).mockResolvedValue(order); + + (fetch as jest.MockedFunction) + .mockResolvedValueOnce( + new Response(JSON.stringify({ access_token: token }), { status: 200 }) + ) + .mockResolvedValueOnce( + new Response(JSON.stringify(statusResponse), { status: 200 }) + ); + + const response = await request(app) + .post('/api/v1/buyer/getPaymentStatus/44') + .send({ requestId: 'testRequestId' }); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + success: true, + message: 'Transaction Done Successfully', + }); + }); + + it('should return 404 if order is not found', async () => { + const token = 'testToken'; + + (orderRepository.findOne as jest.Mock).mockResolvedValue(null); + + (fetch as jest.MockedFunction).mockResolvedValueOnce( + new Response(JSON.stringify({ access_token: token }), { status: 200 }) + ); + + const response = await request(app) + .post('/api/v1/buyer/getPaymentStatus/999') + .send({ requestId: 'testRequestId' }); + + expect(response.status).toBe(404); + expect(response.body).toEqual({ + success: false, + message: 'Order not found', + }); + }); + + it('should return 400 if transaction status is not successful', async () => { + const token = 'testToken'; + const order = { id: 44, paid: false }; + const statusResponse = { + status: 'FAILED', + reason: { message: 'Insufficient funds' }, + }; + + (orderRepository.findOne as jest.Mock).mockResolvedValue(order); + + (fetch as jest.MockedFunction) + .mockResolvedValueOnce( + new Response(JSON.stringify({ access_token: token }), { status: 200 }) + ) + .mockResolvedValueOnce( + new Response(JSON.stringify(statusResponse), { status: 200 }) + ); + + const response = await request(app) + .post('/api/v1/buyer/getPaymentStatus/44') + .send({ requestId: 'testRequestId' }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ + success: false, + message: 'Transaction failed', + reason: { message: 'Insufficient funds' }, + }); + }); + }); +}); diff --git a/src/__test__/coupon.test.ts b/src/__test__/coupon.test.ts index b6d8a61..f1dd8ca 100644 --- a/src/__test__/coupon.test.ts +++ b/src/__test__/coupon.test.ts @@ -2,331 +2,340 @@ 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'); - }); -}); \ No newline at end of file + let token: string; + let couponId: number; + let productId: number; + + beforeAll(async () => { + token = await getVendorToken(); + }); + + it('should create a new coupon with valid data', async () => { + const categoryData = { + name: 'Category', + description: 'category description', + }; + + 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/controller/buyerController.ts b/src/controller/buyerController.ts index 789f623..4d37816 100644 --- a/src/controller/buyerController.ts +++ b/src/controller/buyerController.ts @@ -4,12 +4,15 @@ import Product from '../database/models/productEntity'; import errorHandler from '../middlewares/errorHandler'; import Stripe from 'stripe'; import { Order } from '../database/models/orderEntity'; +import { check } from 'express-validator'; +import crypto from 'crypto'; +import { string } from 'joi'; 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 orderRepository = dbConnection.getRepository(Order); export const getOneProduct = errorHandler( async (req: Request, res: Response) => { @@ -63,3 +66,198 @@ export const handlePayment = errorHandler( return res.status(200).json({ success: true, paid: true, charge }); } ); + +type Idata = { + access_token: string; + token_type: string; + expires_in: string; +}; + +type IStatus = { + amount: string; + currency: string; + externalId: string; + payer: object; + payerMessage: string; + payeeNote: string; + status: string; + reason: object; +}; + +type Ivalidate = { + result: boolean; +}; + +const XRefId = process.env.XRefId as string; +const apiKey = process.env.apiKey as string; +const tokenUrl = process.env.TokenUrl as string; +const subscriptionKey = process.env.subscriptionKey as string; +const requesttoPayUrl = process.env.RequestToPayUrl as string; +const targetEnv = process.env.TargetEnv as string; + +export const purchaseAccessToken = async (): Promise => { + const basicAuth = btoa(`${XRefId}:${apiKey}`); + try { + const response = await fetch(tokenUrl, { + method: 'POST', + headers: { + 'Ocp-Apim-Subscription-Key': subscriptionKey, + 'X-Reference-Id': XRefId, + Authorization: `Basic ${basicAuth}`, + 'Content-Type': 'application/json', + }, + }); + + if (response.ok) { + const data = (await response.json()) as Idata; + return data.access_token; + } else { + console.error('Failed to fetch the access token', response.statusText); + return null; + } + } catch (error) { + console.error('Error occurred while fetching the access token', error); + return null; + } +}; + +export async function requestToPay( + token: string, + xrefid: string, + externalId: string, + currency: string, + amount: string, + number: string, + payerMsg: string, + payeeNote: string +) { + let response = await fetch(requesttoPayUrl, { + method: 'POST', + headers: { + 'Ocp-Apim-Subscription-Key': subscriptionKey, + Authorization: `Bearer ${token}`, + 'X-Target-Environment': targetEnv, + 'X-Reference-Id': xrefid, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + amount: amount, + currency: currency, + externalId: externalId, + payer: { + partyIdType: 'MSISDN', + partyId: number, + }, + payerMessage: payerMsg, + payeeNote: payeeNote, + }), + }); + return response; +} + +export async function requestToPayStatus(id: string, token: string) { + let response = await fetch(`${requesttoPayUrl}/${id}`, { + method: 'GET', + headers: { + 'Ocp-Apim-Subscription-Key': subscriptionKey, + Authorization: `Bearer ${token}`, + 'X-Target-Environment': targetEnv, + }, + }); + let data = (await response.json()) as IStatus; + console.log(data); + return data; +} + +export const validateMomo = async (token: string, momoaccount: string) => { + const validateURL = `https://sandbox.momodeveloper.mtn.com/collection/v1_0/accountholder/msisdn/${momoaccount}/active`; + const resp = await fetch(validateURL, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + 'Ocp-Apim-Subscription-Key': subscriptionKey, + 'X-Target-Environment': targetEnv, + }, + }); + + let response = (await resp.json()) as Ivalidate; + return response.result; +}; + +export const MomohandlePayment = errorHandler( + async (req: Request, res: Response) => { + let token = (await purchaseAccessToken()) as string; + const { orderId, momoNumber } = req.body; + const isValid = await validateMomo(token, momoNumber); + + if (!isValid) { + return res + .status(400) + .json({ message: 'Your Momo Number does not Exist' }); + } + const order = await orderRepository.findOne({ where: { id: orderId } }); + + if (!order) { + 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' }); + } + + let requestId = crypto.randomUUID(); + let externalId = crypto.randomUUID(); + + let response = await requestToPay( + token, + requestId, + externalId, + 'EUR', + order.totalAmount.toString(), + momoNumber, + `paid by ${momoNumber}`, + `paid to ${momoNumber}` + ); + + if (response.ok) { + return res + .status(202) + .json({ message: 'Transaction Accepted', requestId }); + } + return res.status(400).json({ message: 'Transaction Fail' }); + } +); +export const checkPaymentStatus = errorHandler( + async (req: Request, res: Response) => { + const id = parseInt(req.params.id); + const { requestId } = req.body; + const order = await orderRepository.findOne({ where: { id: id } }); + + if (!order) { + return res + .status(404) + .json({ success: false, message: 'Order not found' }); + } + + let token = (await purchaseAccessToken()) as string; + + const data = await requestToPayStatus(requestId, token); + + if (data.status === 'SUCCESSFUL') { + order.paid = true; + await orderRepository.save(order); + return res + .status(200) + .json({ success: true, message: 'Transaction Done Successfully' }); + } + return res.status(400).json({ + success: false, + message: 'Transaction failed', + reason: data.reason, + }); + } +); diff --git a/src/docs/buyerDocs.ts b/src/docs/buyerDocs.ts index c2716a3..412ee60 100644 --- a/src/docs/buyerDocs.ts +++ b/src/docs/buyerDocs.ts @@ -1,10 +1,25 @@ /** * @swagger - * tags: - * name: buyer - * description: Category management + * /api/v1/buyer/get_product/{id}: + * get: + * 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 + * responses: + * '200': + * description: Successful + * '404': + * description: Product not found + * '500': + * description: Internal Server Error */ - /** * @openapi * /buyer/addItemToWishList: @@ -35,57 +50,11 @@ * 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 + * summary: Add an Item to wishList * tags: [Buyer] * security: * - bearerAuth: [] @@ -139,7 +108,7 @@ /** * @swagger - * /api/v1/buyer/getWishList: + * /api/v1/buyer/getAllWishList: * get: * summary: Get a All Wish List * tags: [Buyer] @@ -156,7 +125,7 @@ /** * @swagger - * /api/v1/buyer/getOneWishList: + * /api/v1/buyer/getOneUserWishList: * get: * summary: Get One Wish List * tags: [Buyer] @@ -170,3 +139,139 @@ * '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 + */ + +/** + * @swagger + * /api/v1/buyer/momoPay: + * post: + * summary: Pay Using MTN Momo Pay + * tags: [Buyer] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * momoNumber: + * type: string + * description: Mobile Money Number + * orderId: + * type: number + * description: Order ID + * required: + * - momoNumber + * - orderId + * responses: + * '200': + * description: Successful Transaction + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Transaction was successful + * message: + * type: string + * description: Transaction Accepted + * required: + * - success + * - message + * '400': + * description: Order has already been paid + * '404': + * description: Order not found + * '500': + * description: Internal Server Error + */ +/** + * @swagger + * /api/v1/buyer/getPaymentStatus/{id}: + * post: + * summary: Get Payment Status on MTN Momo Pay + * tags: [Buyer] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * type: number + * required: true + * description: Order ID + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * requestId: + * type: string + * description: Request Id + * responses: + * '200': + * description: Successful + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates whether the transaction was successful + * message: + * type: string + * description: Additional information about the transaction + * '404': + * description: Payment status not found + * '500': + * description: Internal Server Error + */ diff --git a/src/docs/buyerWishlist.ts b/src/docs/buyerWishlist.ts deleted file mode 100644 index 45b8520..0000000 --- a/src/docs/buyerWishlist.ts +++ /dev/null @@ -1,220 +0,0 @@ -/** - * @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/routes/buyerRoutes.ts b/src/routes/buyerRoutes.ts index 0952c5a..457e002 100644 --- a/src/routes/buyerRoutes.ts +++ b/src/routes/buyerRoutes.ts @@ -1,6 +1,10 @@ import { Router } from 'express'; import { checkRole } from '../middlewares/authorize'; -import { getOneProduct } from '../controller/buyerController'; +import { + MomohandlePayment, + checkPaymentStatus, + getOneProduct, +} from '../controller/buyerController'; import { IsLoggedIn } from '../middlewares/isLoggedIn'; import { AddItemInWishList, @@ -12,15 +16,17 @@ import { handlePayment } from '../controller/buyerController'; const buyerRouter = Router(); -buyerRouter.use(IsLoggedIn, checkRole(['Buyer'])); +// 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.get('/getAllWishList', IsLoggedIn, getAllWishList); +buyerRouter.get('/getOneUserWishList', IsLoggedIn, getOneWishList); buyerRouter.post('/payment', handlePayment); +buyerRouter.post('/momoPay', MomohandlePayment); +buyerRouter.post('/getPaymentStatus/:id', checkPaymentStatus); export default buyerRouter;