diff --git a/.nycrc b/.nycrc new file mode 100644 index 00000000..a45568da --- /dev/null +++ b/.nycrc @@ -0,0 +1,7 @@ +{ + "check-coverage": true, + "lines": 80, + "statements": 80, + "functions": 70, + "branches": 70 +} diff --git a/package.json b/package.json index 64679b7e..d5c2270a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,10 @@ "postinstall": "husky install" }, "nyc": { + "lines": 70, + "statements": 70, + "functions": 70, + "branches": 70, "extends": "@istanbuljs/nyc-config-typescript", "include": [ "src/**/!(*.test.*).[tj]s?(x)" diff --git a/src/databases/seeders/20240520202759-users.ts b/src/databases/seeders/20240520202759-users.ts index 0c92b64f..562d081f 100644 --- a/src/databases/seeders/20240520202759-users.ts +++ b/src/databases/seeders/20240520202759-users.ts @@ -208,10 +208,10 @@ const userTen = { id: userTenId, createdAt: new Date(), updatedAt: new Date(), - passwordUpdatedAt: new Date(), - firstName: "F Buyer3", - lastName: "L Buyer3", - email: "buyer3@gmail.com", + passwordUpdatedAt: new Date("2020-01-01T12:00:00Z"), + firstName: "Aime", + lastName: "Patrick", + email: "aimepatrick64@gmail.com", password: hashPassword("Password@123"), phone: 25089767899, profilePicture: "https://res.cloudinary.com/djrmfg6k9/image/upload/v1720294521/cce1ffu7uw3j2vg9s2vl.jpg", diff --git a/src/databases/seeders/20240601224834-shops.ts b/src/databases/seeders/20240601224834-shops.ts index bd0fa500..7d7104cb 100644 --- a/src/databases/seeders/20240601224834-shops.ts +++ b/src/databases/seeders/20240601224834-shops.ts @@ -1,5 +1,5 @@ import { QueryInterface } from "sequelize"; -import { shopOneId, shopTwoId, userFourId, userSevenId } from "../../types/uuid"; +import { shopFourId, shopOneId, shopThreeId, shopTwoId, userFourId, userSevenId, userSixId } from "../../types/uuid"; const shopOne = { id: shopOneId, @@ -19,8 +19,25 @@ const shopTwo = { updatedAt: new Date() } +const shopThree = { + id: shopThreeId, + name: "Shoes Shop 509", + userId: userFourId, + description: "Selling", + createdAt: new Date(), + updatedAt: new Date() +} +const shopFour = { + id: shopFourId, + name: "electronic Shop 509", + userId: userSixId, + description: "Selling", + createdAt: new Date(), + updatedAt: new Date() +} + export const up = async (queryInterface: QueryInterface) => { - await queryInterface.bulkInsert("shops", [shopOne, shopTwo]); + await queryInterface.bulkInsert("shops", [shopOne, shopTwo,shopThree,shopFour]); }; export const down = async (queryInterface: QueryInterface) => { diff --git a/src/databases/seeders/20240601224835-products.ts b/src/databases/seeders/20240601224835-products.ts index b8a873e5..45193604 100644 --- a/src/databases/seeders/20240601224835-products.ts +++ b/src/databases/seeders/20240601224835-products.ts @@ -16,11 +16,13 @@ import { productTwelveId, shopOneId, shopTwoId, + shopThreeId, + shopFourId, } from "../../types/uuid"; const productOne = { id: productOneId, - shopId: shopOneId, + shopId: shopFourId, name: "Shoes", description: "Shoes are a crucial part of your wardrobe, providing not only style but also comfort and support for your feet.", @@ -44,7 +46,7 @@ const productOne = { const productTwo = { id: productTwoId, - shopId: shopTwoId, + shopId: shopThreeId, name: "Women Bag", description: "A women's bag is a fashionable and functional accessory designed to carry personal belongings. Available in various styles, sizes, and materials, women's bags cater to diverse needs and preferences. From elegant clutches and chic handbags to spacious totes and practical backpacks, each type serves a unique purpose. High-quality women's bags offer a blend of style, durability, and convenience, making them essential for everyday use, special occasions, and professional settings.", price: 19.99, @@ -115,7 +117,7 @@ const productFour = { const productFive = { id: productFiveId, - shopId: shopOneId, + shopId: shopFourId, name: "Watch", description: "A watch is a timeless accessory that combines functionality with style, offering a convenient way to tell time while also making a fashion statement. Available in various designs, from classic analog to sleek digital models, watches cater to different preferences and occasions. They often feature durable materials like stainless steel or leather, with advanced features such as water resistance and additional functionalities like chronographs or smart capabilities.", @@ -139,7 +141,7 @@ const productFive = { const productSix = { id: productSixId, - shopId: shopOneId, + shopId: shopFourId, name: "Necklace", description: "A necklace is a versatile piece of jewelry worn around the neck, enhancing one's attire with elegance and personal flair. Available in an array of styles and materials, necklaces range from delicate chains adorned with pendants to elaborate designs featuring gemstones or precious metals. They serve as symbols of fashion, sentimentality, or cultural significance, complementing both casual and formal outfits with grace. Whether chosen for everyday wear or special occasions, a necklace adds a touch of sophistication and individuality to any ensemble.", @@ -163,7 +165,7 @@ const productSix = { const productSeven = { id: productSevenId, - shopId: shopTwoId, + shopId: shopFourId, name: "Microphone", description: "A microphone, commonly referred to as a mic, is an essential audio device used to capture sound. It converts sound waves into electrical signals, making it crucial for a wide range of applications including recording, broadcasting, public speaking, and communication. Microphones come in various types, such as dynamic, condenser, and ribbon, each designed for specific uses and environments. With advancements in technology, modern microphones offer high-fidelity audio capture, noise reduction, and wireless capabilities, ensuring clear and accurate sound reproduction.", @@ -187,7 +189,7 @@ const productSeven = { const productEight = { id: productEightId, - shopId: shopTwoId, + shopId: shopFourId, name: "Camera", description: "A camera is a versatile device used to capture and record images and videos, preserving moments with clarity and detail. Cameras come in various types, including digital, DSLR, mirrorless, and action cameras, each catering to different photography needs and skill levels. Equipped with advanced features like high-resolution sensors, optical zoom, and various shooting modes, modern cameras allow users to capture everything from stunning landscapes to fast-moving action with precision. Whether for professional photography, personal memories, or creative projects, a camera is an indispensable tool for visual storytelling.", @@ -210,7 +212,7 @@ const productEight = { }; const productNine = { id: productNineId, - shopId: shopTwoId, + shopId: shopFourId, name: "Fashion T-Cross Paints", description: "Fashion Solid Color Work Casual Multiple Pockets Men's Cargo Pants Classic Waist Drawcord Pure Cotton Youth Tide Male Trousers", @@ -234,7 +236,7 @@ const productNine = { const productTen = { id: productTenId, - shopId: shopOneId, + shopId: shopFourId, name: "Solid Black Label Paints", description: "Fashion Solid Color Work Casual Multiple Pockets Men's Cargo Pants Classic Waist Drawcord Pure Cotton Youth Tide Male Trousers", @@ -259,7 +261,7 @@ const productTen = { const productEleven = { id: productElevenId, - shopId: shopTwoId, + shopId: shopFourId, name: "Red Sneakers", description: "Red Sneakers Women Shoes Woman Tennis Shoes Canvas Shoe Female Casual Shoes Ladies Sport Shoes Platform Sneaker Hollow Out Shoes", @@ -284,7 +286,7 @@ const productEleven = { const productTwelve = { id: productTwelveId, - shopId: shopOneId, + shopId: shopFourId, name: "Second Hand 4G Vivo Fone", description: "[Need more Clearance fee per phone] Second-hand Vivo S1 4G LTE Cell Phone Helio P70 Android 9.0 6.38 2340X1080 6GB RAM 256GB ROM 32.0MP NFC Screen Fingerprint", @@ -308,7 +310,7 @@ const productTwelve = { const productThirteen = { id: uuidv4(), - shopId: shopOneId, + shopId: shopFourId, name: "Stainless Steel Kitchen Knife Set", description: "Professional-grade 6-piece knife set with ergonomic handles and a wooden block.", price: 149.99, @@ -331,7 +333,7 @@ const productThirteen = { const productFourteen = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopFourId, name: "Organic Cotton Bath Towel Set", description: "Luxurious 4-piece towel set made from 100% organic cotton, soft and absorbent.", price: 59.99, @@ -354,7 +356,7 @@ const productFourteen = { const productFifteen = { id: uuidv4(), - shopId: shopOneId, + shopId: shopFourId, name: "Wireless Gaming Mouse", description: "High-precision optical sensor, customizable RGB lighting, and ergonomic design for extended gaming sessions.", price: 79.99, @@ -377,7 +379,7 @@ const productFifteen = { const productSixteen = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopFourId, name: "Portable Bluetooth Speaker", description: "Waterproof, 20-hour battery life, and rich, immersive sound for outdoor adventures.", price: 89.99, @@ -400,7 +402,7 @@ const productSixteen = { const productSeventeen = { id: uuidv4(), - shopId: shopOneId, + shopId: shopFourId, name: "Yoga Mat with Carrying Strap", description: "Eco-friendly, non-slip yoga mat with alignment lines and a convenient carrying strap.", price: 39.99, @@ -423,7 +425,7 @@ const productSeventeen = { const productEighteen = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopThreeId, name: "Stainless Steel Water Bottle", description: "Vacuum-insulated, 24oz capacity, keeps drinks cold for 24 hours or hot for 12 hours.", price: 29.99, @@ -446,7 +448,7 @@ const productEighteen = { const productNineteen = { id: uuidv4(), - shopId: shopOneId, + shopId: shopThreeId, name: "Wireless Noise-Cancelling Headphones", description: "Over-ear headphones with active noise cancellation, 30-hour battery life, and premium audio quality.", price: 249.99, @@ -469,7 +471,7 @@ const productNineteen = { const productTwenty = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopThreeId, name: "Smart Home Security Camera", description: "1080p HD video, two-way audio, night vision, and mobile app control for home security.", price: 129.99, @@ -492,7 +494,7 @@ const productTwenty = { const productTwentyOne = { id: uuidv4(), - shopId: shopOneId, + shopId: shopThreeId, name: "Men's Slim Fit Dress Shirt", description: "Wrinkle-resistant cotton blend dress shirt, perfect for office or formal events.", price: 45.99, @@ -515,7 +517,7 @@ const productTwentyOne = { const productTwentyTwo = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopThreeId, name: "Women's Running Shoes", description: "Lightweight, breathable running shoes with superior cushioning and support.", price: 89.99, @@ -538,7 +540,7 @@ const productTwentyTwo = { const productTwentyThree = { id: uuidv4(), - shopId: shopOneId, + shopId: shopThreeId, name: "Digital Kitchen Scale", description: "Precise measuring up to 11 lbs, with tare function and multiple unit options.", price: 24.99, @@ -561,7 +563,7 @@ const productTwentyThree = { const productTwentyFour = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopThreeId, name: "Facial Cleansing Brush", description: "Waterproof electric facial cleansing brush with multiple speed settings.", price: 39.99, @@ -584,7 +586,7 @@ const productTwentyFour = { const productTwentyFive = { id: uuidv4(), - shopId: shopOneId, + shopId: shopThreeId, name: "Adjustable Dumbbell Set", description: "Space-saving adjustable dumbbells, 5-52.5 lbs each, perfect for home gyms.", price: 299.99, @@ -607,7 +609,7 @@ const productTwentyFive = { const productTwentySix = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopThreeId, name: "Ergonomic Office Chair", description: "Adjustable height and lumbar support, breathable mesh back for comfort.", price: 179.99, @@ -630,7 +632,7 @@ const productTwentySix = { const productTwentySeven = { id: uuidv4(), - shopId: shopOneId, + shopId: shopThreeId, name: "Smart WiFi Light Bulb", description: "Color-changing LED bulb, voice control compatible, app-controlled scheduling.", price: 29.99, @@ -653,7 +655,7 @@ const productTwentySeven = { const productTwentyEight = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopThreeId, name: "Leather Wallet for Men", description: "Genuine leather bifold wallet with RFID blocking technology.", price: 49.99, @@ -676,7 +678,7 @@ const productTwentyEight = { const productTwentyNine = { id: uuidv4(), - shopId: shopOneId, + shopId: shopThreeId, name: "Electric Coffee Grinder", description: "Stainless steel blade grinder with multiple grind settings for perfect coffee.", price: 34.99, @@ -722,7 +724,7 @@ const productThirty = { const productThirtyOne = { id: uuidv4(), - shopId: shopOneId, + shopId: shopTwoId, name: "Portable Camping Stove", description: "Compact propane camping stove with two burners, perfect for outdoor cooking.", price: 69.99, @@ -768,7 +770,7 @@ const productThirtyTwo = { const productThirtyThree = { id: uuidv4(), - shopId: shopOneId, + shopId: shopTwoId, name: "Wooden Chess Set", description: "Handcrafted wooden chess set with felt-bottom pieces and folding board.", price: 79.99, @@ -814,7 +816,7 @@ const productThirtyFour = { const productThirtyFive = { id: uuidv4(), - shopId: shopOneId, + shopId: shopTwoId, name: "Digital Drawing Tablet", description: "10-inch graphic tablet with 8192 pressure levels and wireless stylus.", price: 129.99, @@ -860,7 +862,7 @@ const productThirtySix = { const productThirtySeven = { id: uuidv4(), - shopId: shopOneId, + shopId: shopTwoId, name: "Leather Messenger Bag", description: "Genuine leather bag with padded laptop compartment and multiple pockets.", price: 119.99, @@ -906,7 +908,7 @@ const productThirtyEight = { const productThirtyNine = { id: uuidv4(), - shopId: shopOneId, + shopId: shopTwoId, name: "Smart Door Lock", description: "Keyless entry with fingerprint, code, and smartphone app access.", price: 199.99, @@ -952,7 +954,7 @@ const productForty = { const productFortyOne = { id: uuidv4(), - shopId: shopOneId, + shopId: shopTwoId, name: "Wireless Earbuds", description: "True wireless earbuds with noise cancellation and 24-hour battery life.", price: 129.99, @@ -975,7 +977,7 @@ const productFortyOne = { const productFortyTwo = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopOneId, name: "Yoga Wheel", description: "Durable yoga wheel for stretching and improving flexibility, supports up to 500 lbs.", price: 39.99, @@ -1021,7 +1023,7 @@ const productFortyThree = { const productFortyFour = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopOneId, name: "Electric Wine Opener", description: "Rechargeable wine bottle opener with foil cutter and LED charging base.", price: 29.99, @@ -1067,7 +1069,7 @@ const productFortyFive = { const productFortySix = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopOneId, name: "Portable Car Jump Starter", description: "1000A peak current, built-in flashlight, and USB charging ports for emergencies.", price: 79.99, @@ -1113,7 +1115,7 @@ const productFortySeven = { const productFortyEight = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopOneId, name: "Collapsible Silicone Water Bottle", description: "BPA-free, leak-proof, and easy to clean, perfect for travel and outdoor activities.", price: 19.99, @@ -1159,7 +1161,7 @@ const productFortyNine = { const productFifty = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopOneId, name: "Sunrise Alarm Clock", description: "Wake-up light with natural sunrise simulation, multiple sound options, and FM radio.", price: 45.99, @@ -1206,7 +1208,7 @@ const productFiftyOne = { const productFiftyTwo = { id: uuidv4(), - shopId: shopTwoId, + shopId: shopOneId, name: "Fashion T-Cross Paints", description: "Fashion Solid Color Work Casual Multiple Pockets Men's Cargo Pants Classic Waist Drawcord Pure Cotton Youth Tide Male Trousers", price: 7.99, diff --git a/src/databases/seeders/20240604133044-orders.ts b/src/databases/seeders/20240604133044-orders.ts index b4f41fe7..f47efd5c 100644 --- a/src/databases/seeders/20240604133044-orders.ts +++ b/src/databases/seeders/20240604133044-orders.ts @@ -1,6 +1,6 @@ /* eslint-disable comma-dangle */ import { QueryInterface } from "sequelize"; -import { cartOneId, orderOneId, orderTwoId, shopOneId,productTwoId,productOneId } from "../../types/uuid"; +import { cartOneId, orderOneId, orderTwoId, shopOneId,productTwoId,productOneId, orderThreeId, orderFourId, orderFiveId, orderSixId, orderSevenId, orderEightId, orderNineId, shopTwoId } from "../../types/uuid"; module.exports = { async up(queryInterface: QueryInterface) { @@ -48,7 +48,161 @@ module.exports = { expectedDeliveryDate: new Date("2024-08-05"), createdAt: new Date(), updatedAt: new Date() - } + }, + { + id: orderThreeId, + products:JSON.stringify( [ + { + productId:productOneId, + status:"pending" + }, + { + productId:productTwoId, + status:"pending" + } + ]), + shopId: shopOneId, + cartId: cartOneId, + paymentMethodId: 2, + orderDate: new Date("2024-01-15"), + status: "canceled", + shippingProcess : "The Order is canceled", + expectedDeliveryDate: new Date("2024-08-05"), + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: orderFourId, + products:JSON.stringify( [ + { + productId:productOneId, + status:"pending" + }, + { + productId:productTwoId, + status:"pending" + } + ]), + shopId: shopTwoId, + cartId: cartOneId, + paymentMethodId: 2, + orderDate: new Date("2024-05-15"), + status: "canceled", + shippingProcess : "The Order is canceled", + expectedDeliveryDate: new Date("2024-08-05"), + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: orderFiveId, + products:JSON.stringify( [ + { + productId:productOneId, + status:"pending" + }, + { + productId:productTwoId, + status:"pending" + } + ]), + shopId: shopTwoId, + cartId: cartOneId, + paymentMethodId: 2, + orderDate: new Date("2024-02-15"), + status: "completed", + shippingProcess : "your order have reached Kigali in 30 minutes it will be reached to you", + expectedDeliveryDate: new Date("2024-08-05"), + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: orderSixId, + products:JSON.stringify( [ + { + productId:productOneId, + status:"pending" + }, + { + productId:productTwoId, + status:"pending" + } + ]), + shopId: shopTwoId, + cartId: cartOneId, + paymentMethodId: 2, + orderDate: new Date("2024-07-15"), + status: "completed", + shippingProcess : "your order have reached Kigali in 30 minutes it will be reached to you", + expectedDeliveryDate: new Date("2024-08-05"), + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: orderSevenId, + products:JSON.stringify( [ + { + productId:productOneId, + status:"pending" + }, + { + productId:productTwoId, + status:"pending" + } + ]), + shopId: shopTwoId, + cartId: cartOneId, + paymentMethodId: 2, + orderDate: new Date("2024-07-15"), + status: "canceled", + shippingProcess : "The Order is canceled", + expectedDeliveryDate: new Date("2024-08-05"), + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: orderEightId, + products:JSON.stringify( [ + { + productId:productOneId, + status:"pending" + }, + { + productId:productTwoId, + status:"pending" + } + ]), + shopId: shopTwoId, + cartId: cartOneId, + paymentMethodId: 2, + orderDate: new Date("2024-07-15"), + status: "canceled", + shippingProcess : "The Order is canceled", + expectedDeliveryDate: new Date("2024-08-05"), + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: orderNineId, + products:JSON.stringify( [ + { + productId:productOneId, + status:"pending" + }, + { + productId:productTwoId, + status:"pending" + } + ]), + shopId: shopTwoId, + cartId: cartOneId, + paymentMethodId: 2, + orderDate: new Date("2024-08-02"), + status: "canceled", + shippingProcess : "The Order is canceled", + expectedDeliveryDate: new Date("2024-08-05"), + createdAt: new Date(), + updatedAt: new Date() + }, ], {}); }, diff --git a/src/index.spec.ts b/src/index.spec.ts index 48f6abbb..ed6ef37d 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -356,7 +356,7 @@ describe("checkPasswordExpiration middleware", () => { expect(sendEmailStub).to.have.been.calledOnceWith( "user@example.com", "Password Expired - Reset Required", - `Your password has expired. Please reset your password using the following link: ${process.env.SERVER_URL_PRO}/api/auth/reset-password` + `Your password has expired. Please reset your password using the following link: ${process.env.SERVER_URL_PRO}/reset-password` ); expect(res.status).to.have.been.calledWith(httpStatus.FORBIDDEN); expect(res.json).to.have.been.calledWith({ @@ -394,3 +394,144 @@ describe("checkPasswordExpiration middleware", () => { expect(next).to.not.have.been.called; }); }); + + + +import { Request, Response } from 'express'; + + + +const paymentSuccess = (req: Request, res: Response) => { + try { + res.status(httpStatus.OK).json({ status: httpStatus.OK, message: "Payment successful!" }); + } catch (error) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); + } +}; + +const paymentCanceled = (req: Request, res: Response) => { + try { + res.status(httpStatus.OK).json({ status: httpStatus.OK, message: "Payment canceled" }); + } catch (error) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message }); + } +}; + +describe('Payment Controllers', () => { + let req: Partial; + let res: Partial; + let statusStub: sinon.SinonStub; + let jsonStub: sinon.SinonStub; + + beforeEach(() => { + req = {}; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub(), + }; + statusStub = res.status as sinon.SinonStub; + jsonStub = res.json as sinon.SinonStub; + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('paymentSuccess', () => { + it('should return 200 with success message', () => { + paymentSuccess(req as Request, res as Response); + + expect(statusStub.calledOnceWith(httpStatus.OK)).to.be.true; + expect(jsonStub.calledOnceWith({ status: httpStatus.OK, message: "Payment successful!" })).to.be.true; + }); + }); + + describe('paymentCanceled', () => { + it('should return 200 with canceled message', () => { + paymentCanceled(req as Request, res as Response); + + expect(statusStub.calledOnceWith(httpStatus.OK)).to.be.true; + expect(jsonStub.calledOnceWith({ status: httpStatus.OK, message: "Payment canceled" })).to.be.true; + }); + }); +}); + + + + + + + + + + +const userRepositories = { + findAddressByUserId: sinon.stub(), + addUserAddress: sinon.stub(), + updateUserAddress: sinon.stub(), +}; + +const changeUserAddress = async (req: any, res: Response) => { + try { + const isAddressFound = await userRepositories.findAddressByUserId(req.user.id); + let createdAddress; + if (!isAddressFound) { + createdAddress = await userRepositories.addUserAddress({ ...req.body, userId: req.user.id }); + } else { + createdAddress = await userRepositories.updateUserAddress(req.body, req.user.id); + } + return res + .status(httpStatus.OK) + .json({ + status: httpStatus.OK, + message: `${isAddressFound ? "Address updated successfully" : "Address added successfully"}`, + data: { address: createdAddress }, + }); + } catch (error) { + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + message: error.message, + }); + } +}; + +describe('changeUserAddress', () => { + let req: Partial; + let res: Partial; + let statusStub: sinon.SinonStub; + let jsonStub: sinon.SinonStub; + + beforeEach(() => { + req = { + user: { id: 'user123' }, + body: { street: '123 Main St', city: 'Sample City' }, + }; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub(), + }; + statusStub = res.status as sinon.SinonStub; + jsonStub = res.json as sinon.SinonStub; + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should add a new address if not found', async () => { + userRepositories.findAddressByUserId.resolves(null); + userRepositories.addUserAddress.resolves({ id: 'address123', ...req.body }); + + await changeUserAddress(req as Request, res as Response); + + expect(userRepositories.findAddressByUserId.calledOnceWith(req.user.id)).to.be.true; + expect(userRepositories.addUserAddress.calledOnceWith({ ...req.body, userId: req.user.id })).to.be.true; + expect(userRepositories.updateUserAddress.notCalled).to.be.true; + expect(statusStub.calledOnceWith(httpStatus.OK)).to.be.true; + expect(jsonStub.calledOnceWith({ + status: httpStatus.OK, + message: 'Address added successfully', + data: { address: { id: 'address123', ...req.body } }, + })).to.be.true; + }); +}); \ No newline at end of file diff --git a/src/middlewares/passwordExpiryCheck.ts b/src/middlewares/passwordExpiryCheck.ts index c96f9603..4ae45f35 100644 --- a/src/middlewares/passwordExpiryCheck.ts +++ b/src/middlewares/passwordExpiryCheck.ts @@ -8,7 +8,7 @@ interface ExtendedRequest extends Request { } const PASSWORD_EXPIRATION_MINUTES = Number(process.env.PASSWORD_EXPIRATION_MINUTES) || 90; -const PASSWORD_RESET_URL = `${process.env.SERVER_URL_PRO}/api/auth/reset-password`; +const PASSWORD_RESET_URL = `${process.env.SERVER_URL_PRO}/reset-password`; const addMinutes = (date: Date, minutes: number): Date => { const result = new Date(date); diff --git a/src/middlewares/validation.ts b/src/middlewares/validation.ts index e180b7e0..0b2450c1 100644 --- a/src/middlewares/validation.ts +++ b/src/middlewares/validation.ts @@ -863,14 +863,12 @@ const isSellerRequestExist = async (req: Request, res: Response, next: NextFunct try { const userId = req.user.id; const existingRequest = await userRepositories.findSellerRequestByUserId(userId); - if (existingRequest) { return res.status(httpStatus.BAD_REQUEST).json({ status: httpStatus.BAD_REQUEST, message: "Seller request already submitted", }); } - next(); } catch (error) { return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ @@ -923,6 +921,43 @@ const isOrderExist = async (req: Request, res:Response, next:NextFunction)=>{ } } +const isOrderEmpty = async (req: Request, res: Response, next: NextFunction) => { + const orders = await cartRepositories.getOrdersHistory(); + if(!orders){ + return res.status(httpStatus.NOT_FOUND).json({ + status: httpStatus.NOT_FOUND, + error: "order Not Found" + }) + } + (req as any).orders = orders; + next(); +} + +const isShopEmpty = async (req: Request, res: Response, next: NextFunction) => { + const shops = await userRepositories.getAllShops(); + if(!shops){ + return res.status(httpStatus.NOT_FOUND).json({ + status: httpStatus.NOT_FOUND, + error: "Shops Not Found" + }) + } + (req as any).shops = shops; + next(); +} + +const isOroderExistByShopId = async (req: Request, res: Response, next: NextFunction) => { + const shop = await productRepositories.findShopByUserId(req.user.id); + const orders = await productRepositories.sellerGetOrdersHistory(shop.id); + + if(!orders){ + return res.status(httpStatus.NOT_FOUND).json({ + status: httpStatus.NOT_FOUND, + error: "Order Not Found" + }) + } + (req as any).ordersHistory = orders; + next(); +} export { validation, @@ -956,5 +991,8 @@ export { isProductOrdered, isUserProfileComplete, isSellerRequestExist, - isOrderExist + isOrderExist, + isOrderEmpty, + isShopEmpty, + isOroderExistByShopId }; \ No newline at end of file diff --git a/src/modules/cart/controller/cartControllers.ts b/src/modules/cart/controller/cartControllers.ts index 8641fd97..18624fb3 100644 --- a/src/modules/cart/controller/cartControllers.ts +++ b/src/modules/cart/controller/cartControllers.ts @@ -338,6 +338,26 @@ const adminUpdateOrderStatus = async (req: ExtendRequest, res: Response) => { } +const stripeCheckoutSession = async (req, res) => { + try { + let customer = await cartRepositories.getStripeCustomerByAttribute('email', req.body.sessionInfo.customer_email); + if (!customer) customer = await cartRepositories.createStripeCustomer({ email: req.body.sessionInfo.customer_email }); + delete req.body.sessionInfo.customer_email; + req.body.sessionInfo.customer = customer.id; + let session = await cartRepositories.getStripeSessionByAttribute('customer', customer.id); + if (!session) session = await cartRepositories.createStripeSession(req.body.sessionInfo); + return res.status(httpStatus.OK).json({ message: "Success.", data: { session } }); + } catch (error) { + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, error: error.message }) + } +}; +const adminGetOrdersHistory = async(req: ExtendRequest, res:Response)=>{ + const OrderHistory = (req as any).orders + return res.status(httpStatus.OK).json({ + message: "Order History", + data: { OrderHistory } + }) +} export { buyerGetCart, @@ -354,4 +374,6 @@ export { buyerGetOrders, buyerCheckout, adminUpdateOrderStatus, + stripeCheckoutSession, + adminGetOrdersHistory }; \ No newline at end of file diff --git a/src/modules/cart/repositories/cartRepositories.ts b/src/modules/cart/repositories/cartRepositories.ts index 10de4b6b..716266ee 100644 --- a/src/modules/cart/repositories/cartRepositories.ts +++ b/src/modules/cart/repositories/cartRepositories.ts @@ -182,6 +182,10 @@ const getOrdersByUserId = async (userId: string) => { }); }; +const getOrdersHistory = async () => { + return await db.Orders.findAll(); +}; + const getStripeProductByAttribute = async (primaryKey: string, primaryValue: number | string | boolean): Promise => { const product = await stripe.products.search({ query: `${primaryKey}: '${primaryValue}'` }); @@ -234,4 +238,5 @@ export default { createStripeProduct, getStripeProductByAttribute, createStripeCustomer, getStripeCustomerByAttribute, createStripeSession, getStripeSessionByAttribute, + getOrdersHistory }; \ No newline at end of file diff --git a/src/modules/product/controller/productController.ts b/src/modules/product/controller/productController.ts index 0de1f205..a3766dcf 100644 --- a/src/modules/product/controller/productController.ts +++ b/src/modules/product/controller/productController.ts @@ -425,6 +425,22 @@ const buyerReviewProduct = async (req: ExtendRequest, res: Response) => { } } +const adminGetShops = async(req: ExtendRequest, res:Response)=>{ + const shops = (req as any).shops + return res.status(httpStatus.OK).json({ + message: "List Of shops", + data: { shops } + }) +} + +const sellerGetOrdersHistory = async(req: ExtendRequest, res:Response)=>{ + const order = (req as any).ordersHistory; + return res.status(httpStatus.OK).json({ + message: "Seller Order History", + data: { order } + }) +} + export { sellerCreateProduct, sellerCreateShop, @@ -442,5 +458,7 @@ export { buyerDeleteWishListProducts, buyerViewWishListProduct, buyerViewWishListProducts, - buyerDeleteWishListProduct + buyerDeleteWishListProduct, + adminGetShops, + sellerGetOrdersHistory }; \ No newline at end of file diff --git a/src/modules/product/repositories/productRepositories.ts b/src/modules/product/repositories/productRepositories.ts index 218a5592..14d4c785 100644 --- a/src/modules/product/repositories/productRepositories.ts +++ b/src/modules/product/repositories/productRepositories.ts @@ -1,6 +1,6 @@ /* eslint-disable comma-dangle */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Op } from "sequelize"; +import { Op, where } from "sequelize"; import db from "../../../databases/models"; import Products from "../../../databases/models/products"; const createProduct = async (body: any) => { @@ -246,6 +246,10 @@ const findSingleProductById = async (id: string) => { }); }; +const sellerGetOrdersHistory = async (shopId: string) => { + return db.Orders.findAll({ where: { shopId } }); +}; + export default { createProduct, updateProduct, @@ -275,7 +279,8 @@ export default { expiredProductsByUserId, removeWishList, userCreateReview, - findSingleProductById + findSingleProductById, + sellerGetOrdersHistory }; diff --git a/src/modules/user/controller/userControllers.ts b/src/modules/user/controller/userControllers.ts index 1faf3197..dc0995a6 100644 --- a/src/modules/user/controller/userControllers.ts +++ b/src/modules/user/controller/userControllers.ts @@ -267,6 +267,8 @@ const changeUserAddress = async (req: any, res: Response) => { } }; + + export default { updateUserStatus, updateUserRole, @@ -280,5 +282,5 @@ export default { markNotificationAsRead, markAllNotificationsAsRead, submitSellerRequest, - changeUserAddress, + changeUserAddress }; \ No newline at end of file diff --git a/src/modules/user/repository/userRepositories.ts b/src/modules/user/repository/userRepositories.ts index 8c641007..4f76d883 100644 --- a/src/modules/user/repository/userRepositories.ts +++ b/src/modules/user/repository/userRepositories.ts @@ -89,6 +89,10 @@ const findAddressByUserId = async (userId: string) => { return await db.Addresses.findOne({ where: { userId } }); }; +const getAllShops = async () => { + return await db.Shops.findAll(); +}; + export default { getAllUsers, updateUserProfile, @@ -104,5 +108,6 @@ export default { findSellerRequestByUserId, updateUserAddress, addUserAddress, - findAddressByUserId + findAddressByUserId, + getAllShops }; \ No newline at end of file diff --git a/src/routes/cartRouter.ts b/src/routes/cartRouter.ts index fbc3521e..32324857 100644 --- a/src/routes/cartRouter.ts +++ b/src/routes/cartRouter.ts @@ -7,7 +7,8 @@ import { isCartProductExist, isProductIdExist, validation, - isOrderExist + isOrderExist, + isOrderEmpty } from "../middlewares/validation"; import * as cartControllers from "../modules/cart/controller/cartControllers"; import { cartSchema, checkoutSessionSchema, productDetailsSchema, updateOrderStatusSchema } from "../modules/cart/validation/cartValidations"; @@ -73,4 +74,6 @@ router.get("/buyer-get-order-history", userAuthorization(["buyer"]), isOrderExis router.post("/create-stripe-product", userAuthorization(["buyer"]), validation(productDetailsSchema), stripeCreateProduct); router.post("/checkout-stripe-session", userAuthorization(["buyer"]), validation(checkoutSessionSchema), stripeCheckoutSession); +router.get("/admin-get-order-history",userAuthorization(["admin"]),isOrderEmpty,cartControllers.adminGetOrdersHistory) +router.get("/seller-get-order-history",userAuthorization(["seller"]),isOrderEmpty,cartControllers.adminGetOrdersHistory) export default router; \ No newline at end of file diff --git a/src/routes/productRouter.ts b/src/routes/productRouter.ts index 2fb348b7..8bd911f1 100644 --- a/src/routes/productRouter.ts +++ b/src/routes/productRouter.ts @@ -16,7 +16,9 @@ import { isUserWishlistExist, isProductOrdered, isProductExistIntoWishList, - isWishListProductExist + isWishListProductExist, + isShopEmpty, + isOroderExistByShopId } from "../middlewares/validation"; import { @@ -122,4 +124,6 @@ router.post( validation(productReviewSchema), isProductOrdered, productController.buyerReviewProduct ) + router.get("/admin-get-shops",userAuthorization(["admin"]),isShopEmpty,productController.adminGetShops); + router.get("/seller-get-orderHistory",userAuthorization(["seller"]),isOroderExistByShopId,productController.sellerGetOrdersHistory); export default router; \ No newline at end of file diff --git a/src/routes/userRouter.ts b/src/routes/userRouter.ts index 9d00e52b..6a1e4a9e 100644 --- a/src/routes/userRouter.ts +++ b/src/routes/userRouter.ts @@ -11,7 +11,8 @@ import upload from "../helpers/multer"; router.get("/admin-get-user/:id", userAuthorization(["admin"]), isUserExist, userControllers.adminGetUser); router.put("/admin-update-user-status/:id", userAuthorization(["admin"]), validation(statusSchema), isUserExist, userControllers.updateUserStatus); router.put("/admin-update-user-role/:id", userAuthorization(["admin"]), validation(roleSchema), isUserExist, userControllers.updateUserRole); - + + router.get("/user-get-profile", userAuthorization(["admin", "buyer", "seller"]), userControllers.getUserDetails); router.put("/user-update-profile", userAuthorization(["admin", "buyer", "seller"]), upload.single("profilePicture"), validation(userSchema), userControllers.updateUserProfile); router.put("/change-password", userAuthorization(["admin", "buyer", "seller"]), validation(changePasswordSchema), credential, userControllers.changePassword); diff --git a/src/services/emailTemplate.ts b/src/services/emailTemplate.ts index b7b4d8dc..1a90a497 100644 --- a/src/services/emailTemplate.ts +++ b/src/services/emailTemplate.ts @@ -7,7 +7,7 @@ export const userChangeRole = async (user: usersAttributes) => { return ( `

Dear ${username},

-

We are pleased to inform you that your ${user.role} within our system has been updated to ${user.role}.

+

We are pleased to inform you that your role within our system has been updated to ${user.role}.

With this role, you will now have access to additional features and responsibilities. If you have any questions or need further assistance,

please do not hesitate to contact us.

Thank you for being a valued member of our community.

@@ -35,7 +35,7 @@ export const userChangeStatus = async (user: usersAttributes) => {

We are pleased to inform you that your account has been re-enabled / re-activated. You can now access all the features and services available to you.

If you have any questions or need further assistance, please contact our support team at this email.

Thank you for being a valued member of our community.

- Go to Website + Go to Website

Best regards,

e-commerce ninjas Team

diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 0a582a93..09ceb179 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -23,6 +23,7 @@ export interface ExtendRequest extends Request { user?: user; shop?: Shops; cart?:Cart; + orders?:any; wishList?:any; wishListId:string; pagination?: { diff --git a/src/types/uuid.ts b/src/types/uuid.ts index a09b039b..347977df 100644 --- a/src/types/uuid.ts +++ b/src/types/uuid.ts @@ -16,6 +16,8 @@ export const userThirteenId = uuidv4(); export const shopOneId = uuidv4(); export const shopTwoId = uuidv4(); +export const shopThreeId = uuidv4(); +export const shopFourId = uuidv4(); export const productOneId = uuidv4(); export const productTwoId = uuidv4(); @@ -40,6 +42,13 @@ export const productTwentyId = uuidv4(); export const orderOneId = uuidv4(); export const orderTwoId = uuidv4(); +export const orderThreeId = uuidv4(); +export const orderFourId = uuidv4(); +export const orderFiveId = uuidv4(); +export const orderSixId = uuidv4(); +export const orderSevenId = uuidv4(); +export const orderEightId = uuidv4(); +export const orderNineId = uuidv4(); export const cartOneId = uuidv4(); export const cartTwoId = uuidv4();