diff --git a/contributors.md b/contributors.md index a27e5ab6..d5e4e5b0 100644 --- a/contributors.md +++ b/contributors.md @@ -1,4 +1,5 @@ [Nainah23](https://github.com/Nainah23) Erasmus Tayviah (StarmannRassy) [Adekolu Samuel Samixx](https://github.com/samixYasuke) -[Micheal Peter] (https://github.com/myconpeter) +[Micheal Peter] () +[Adetayo Adewobi] () diff --git a/q b/q new file mode 100644 index 00000000..93e8f38c --- /dev/null +++ b/q @@ -0,0 +1,7 @@ + dev + feat/fetch-all-org-invite + feat/generate-invite-token + feat/update-blog + feat/update-faq +* feat/update-product + fix/get-all-newsletter diff --git a/src/controllers/ProductController.ts b/src/controllers/ProductController.ts index 24d534b1..9f0a006b 100644 --- a/src/controllers/ProductController.ts +++ b/src/controllers/ProductController.ts @@ -452,6 +452,31 @@ class ProductController { return new BadRequest("Invalid Product ID"); } }; + + public updateProduct = async (req: Request, res: Response) => { + try { + const orgId = req.params.orgId; + const productId = req.params.productId; + const updatedProductData = req.body; + const userId = req.user.id; + + const updatedProduct = await this.productService.updateProduct( + orgId, + productId, + updatedProductData, + userId, + ); + + res.status(200).json(updatedProduct); + } catch (error) { + console.log(error); + res.status(error.status_code || 500).json({ + status_code: error.status_code || 500, + message: error.message || "An unexpected error occurred", + data: {}, + }); + } + }; } export { ProductController }; diff --git a/src/models/product.ts b/src/models/product.ts index aadd5761..0d7d630a 100644 --- a/src/models/product.ts +++ b/src/models/product.ts @@ -11,7 +11,7 @@ import ExtendedBaseEntity from "./extended-base-entity"; import { ProductSize, StockStatus } from "../enums/product"; import { User } from "./user"; @Entity() -export class Product extends ExtendedBaseEntity { +export class Product { @PrimaryGeneratedColumn("uuid") id: string; diff --git a/src/routes/product.ts b/src/routes/product.ts index a0a03110..ee732b55 100644 --- a/src/routes/product.ts +++ b/src/routes/product.ts @@ -1,9 +1,8 @@ import { Router } from "express"; import { ProductController } from "../controllers/ProductController"; -import { authMiddleware, validOrgAdmin } from "../middleware"; +import { authMiddleware, checkPermissions, validOrgAdmin } from "../middleware"; import { validateProductDetails } from "../middleware/product"; import { validateUserToOrg } from "../middleware/organizationValidation"; -import { adminOnly } from "../middleware"; const productRouter = Router(); const productController = new ProductController(); @@ -37,5 +36,9 @@ productRouter.get( validOrgAdmin, productController.getSingleProduct, ); - +productRouter.patch( + "/organisations/:orgId/products/:productId", + authMiddleware, + productController.updateProduct, +); export { productRouter }; diff --git a/src/services/product.services.ts b/src/services/product.services.ts index daffacd0..d316c62f 100644 --- a/src/services/product.services.ts +++ b/src/services/product.services.ts @@ -6,14 +6,17 @@ import { InvalidInput, ResourceNotFound, ServerError, + Unauthorized, } from "../middleware"; import { Organization } from "../models/organization"; import { Product } from "../models/product"; import { ProductSchema } from "../schema/product.schema"; +import { User } from "../models"; export class ProductService { private productRepository: Repository; private organizationRepository: Repository; + private userRepository: Repository; private entities: { [key: string]: { @@ -25,6 +28,7 @@ export class ProductService { constructor() { this.productRepository = AppDataSource.getRepository(Product); this.organizationRepository = AppDataSource.getRepository(Organization); + this.userRepository = AppDataSource.getRepository(User); this.entities = { product: { @@ -198,4 +202,65 @@ export class ProductService { throw new ResourceNotFound(error.message); } } + + public async updateProduct( + orgId: string, + productId: string, + payload: ProductSchema, + userId: any, + ) { + const user = await this.userRepository.findOne({ where: { id: userId } }); + if (!user) { + throw new ResourceNotFound("User not found"); + } + if (user.role !== "admin") { + throw new Unauthorized("Access denied. Admins only"); + } + + const organization = await this.organizationRepository.findOne({ + where: { id: orgId }, + }); + if (!organization) { + throw new ResourceNotFound("Invalid organization credentials"); + } + let product; + product = await this.productRepository.findOne({ + where: { id: productId }, + relations: ["org"], + }); + + if (!product || product.org.id !== orgId) { + throw new ResourceNotFound( + "Product not found or does not belong to the organization", + ); + } + product.stock_status = await this.calculateProductStatus(payload.quantity); + + Object.assign(product, payload); + + const updatedProduct = await this.productRepository.save(product); + if (!updatedProduct) { + throw new ServerError( + "An unexpected error occurred. Please try again later.", + ); + } + + return { + status_code: 200, + message: "Product updated successfully", + data: { + id: updatedProduct.id, + name: updatedProduct.name, + description: updatedProduct.description, + price: updatedProduct.price, + category: updatedProduct.category, + image: updatedProduct.image, + quantity: updatedProduct.quantity, + size: updatedProduct.size, + stock_status: updatedProduct.stock_status, + created_at: updatedProduct.created_at, + updated_at: updatedProduct.updated_at, + }, + }; + } } diff --git a/src/test/product.spec.ts b/src/test/product.spec.ts index 2baf0ad1..ce88d3ca 100644 --- a/src/test/product.spec.ts +++ b/src/test/product.spec.ts @@ -33,11 +33,13 @@ describe("ProductService", () => { let productService: ProductService; let productRepository: Repository; let organizationRepository: Repository; + let userRepository: Repository; beforeEach(() => { productService = new ProductService(); productRepository = productService["productRepository"]; organizationRepository = productService["organizationRepository"]; + userRepository = productService["userRepository"]; }); describe("createProducts", () => { diff --git a/src/test/updateProduct.spec.ts b/src/test/updateProduct.spec.ts new file mode 100644 index 00000000..70d7bdc5 --- /dev/null +++ b/src/test/updateProduct.spec.ts @@ -0,0 +1,168 @@ +import { ProductService } from "../services"; +import { Repository } from "typeorm"; +import { Product, User, Organization } from "../models"; +import AppDataSource from "../data-source"; +import { ResourceNotFound, Unauthorized, ServerError } from "../middleware"; +import { ProductSchema } from "../schema/product.schema"; + +jest.mock("../data-source"); + +describe("ProductService", () => { + let productService: ProductService; + let productRepository: jest.Mocked>; + let userRepository: jest.Mocked>; + let organizationRepository: jest.Mocked>; + + beforeEach(() => { + productRepository = { + findOne: jest.fn(), + save: jest.fn(), + } as any; + + userRepository = { + findOne: jest.fn(), + } as any; + + organizationRepository = { + findOne: jest.fn(), + } as any; + + AppDataSource.getRepository = jest.fn().mockImplementation((model) => { + if (model === Product) return productRepository; + if (model === User) return userRepository; + if (model === Organization) return organizationRepository; + throw new Error("Unknown model"); + }); + + productService = new ProductService(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("updateProduct", () => { + it("should successfully update a product", async () => { + const orgId = "org-123"; + const productId = "product-123"; + const userId = "admin-user"; + const payload = { + name: "Updated Product", + description: "Updated description", + price: 100, + category: "Updated category", + image: "updated-image.jpg", + quantity: 20, + size: "L", + }; + + const mockUser = { id: userId, role: "admin" } as User; + const mockOrganization = { id: orgId } as Organization; + const existingProduct = { + id: productId, + org: { id: orgId }, + stock_status: "in stock", + ...payload, + created_at: new Date(), + updated_at: new Date(), + } as Product; + + productRepository.findOne.mockResolvedValue(existingProduct); + userRepository.findOne.mockResolvedValue(mockUser); + organizationRepository.findOne.mockResolvedValue(mockOrganization); + productRepository.save.mockResolvedValue(existingProduct); + + const result = await productService.updateProduct( + orgId, + productId, + payload, + userId, + ); + + expect(userRepository.findOne).toHaveBeenCalledWith({ + where: { id: userId }, + }); + expect(organizationRepository.findOne).toHaveBeenCalledWith({ + where: { id: orgId }, + }); + expect(productRepository.findOne).toHaveBeenCalledWith({ + where: { id: productId }, + relations: ["org"], + }); + expect(productRepository.save).toHaveBeenCalledWith(existingProduct); + expect(result).toEqual({ + status_code: 200, + message: "Product updated successfully", + data: { + id: productId, + stock_status: "in stock", + ...payload, + created_at: existingProduct.created_at, + updated_at: existingProduct.updated_at, + }, + }); + }); + + it("should throw ResourceNotFound if the user does not exist", async () => { + const orgId = "org-123"; + const productId = "product-123"; + const userId = "non-existent-user"; + const payload = {} as Product; + + userRepository.findOne.mockResolvedValue(null); + + await expect( + productService.updateProduct(orgId, productId, payload, userId), + ).rejects.toThrow(ResourceNotFound); + }); + + it("should throw Unauthorized if the user is not an admin", async () => { + const orgId = "org-123"; + const productId = "product-123"; + const userId = "regular-user"; + const payload = {} as Product; + + const mockUser = { id: userId, role: "user" } as User; + userRepository.findOne.mockResolvedValue(mockUser); + + await expect( + productService.updateProduct(orgId, productId, payload, userId), + ).rejects.toThrow(Unauthorized); + }); + + it("should throw ResourceNotFound if the organization does not exist", async () => { + const orgId = "org-123"; + const productId = "product-123"; + const userId = "admin-user"; + const payload = {} as Product; + + const mockUser = { id: userId, role: "admin" } as User; + userRepository.findOne.mockResolvedValue(mockUser); + organizationRepository.findOne.mockResolvedValue(null); + + await expect( + productService.updateProduct(orgId, productId, payload, userId), + ).rejects.toThrow(ResourceNotFound); + }); + + it("should throw ResourceNotFound if the product does not belong to the organization or does not exist", async () => { + const orgId = "org-123"; + const productId = "product-123"; + const userId = "admin-user"; + const payload = {} as Product; + + const mockUser = { id: userId, role: "admin" } as User; + const mockOrganization = { id: orgId } as Organization; + userRepository.findOne.mockResolvedValue(mockUser); + organizationRepository.findOne.mockResolvedValue(mockOrganization); + productRepository.findOne.mockResolvedValue({ + id: productId, + org: { id: "other-org-id" }, + } as Product); + + await expect( + productService.updateProduct(orgId, productId, payload, userId), + ).rejects.toThrow(ResourceNotFound); + }); + }); +});