From ce768059fcf81eb331f21c067fa16a2125bddc81 Mon Sep 17 00:00:00 2001 From: Adeosun Oluwaseyi Date: Wed, 7 Aug 2024 22:54:47 +0100 Subject: [PATCH 1/2] feat: create product --- src/controllers/ProductController.ts | 228 --------------------------- src/routes/product.ts | 15 -- src/services/product.services.ts | 62 -------- src/test/product.spec.ts | 117 -------------- 4 files changed, 422 deletions(-) diff --git a/src/controllers/ProductController.ts b/src/controllers/ProductController.ts index ad4291a7..3ab67e43 100644 --- a/src/controllers/ProductController.ts +++ b/src/controllers/ProductController.ts @@ -105,234 +105,6 @@ class ProductController { .status(201) .json({ message: "Product created successfully", data: newProduct }); }; - - /** - * @openapi - * /api/v1/organisation/{:id}/product/search: - * get: - * tags: - * - Product API - * summary: Get All products. - * description: Get All products within an organisation. - * parameters: - * - name: org_id - * in: path - * required: true - * description: ID of the organisation - * schema: - * type: string - * - name: name - * in: query - * required: false - * description: Name of the product - * schema: - * type: string - * - name: category - * in: query - * required: false - * description: Category of the product - * schema: - * type: string - * - name: minPrice - * in: query - * required: false - * description: Minimum price of the product - * schema: - * type: number - * - name: maxPrice - * in: query - * required: false - * description: Maximum price of the product - * schema: - * type: number - * - name: page - * in: query - * required: false - * description: Page number for pagination - * schema: - * type: number - * default: 1 - * - name: limit - * in: query - * required: false - * description: Number of results per page - * schema: - * type: number - * default: 10 - * responses: - * 200: - * description: Product search successful - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "Product search successful" - * data: - * type: object - * properties: - * pagination: - * type: object - * properties: - * total: - * type: number - * page: - * type: number - * limit: - * type: number - * totalPages: - * type: number - * products: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * name: - * type: string - * description: - * type: string - * price: - * type: number - * category: - * type: string - * status: - * type: string - * quantity: - * type: number - * created_at: - * type: string - * format: date-time - * updated_at: - * type: string - * format: date-time - * 400: - * description: Invalid input or missing organisation ID - * 404: - * description: No products found - */ - - public getProduct = async ( - req: Request, - res: Response, - next: NextFunction, - ) => { - try { - const orgId = req.params.org_id; - const { - name, - category, - minPrice, - maxPrice, - page = 1, - limit = 10, - } = req.query as any; - const searchCriteria = { - name, - category, - minPrice: Number(minPrice), - maxPrice: Number(maxPrice), - }; - - const products = await this.productService.getProducts( - orgId, - searchCriteria, - Number(page), - Number(limit), - ); - return res - .status(200) - .json({ message: "Product search successful", data: products }); - } catch (error) { - next(error); - } - }; - - /** - * @openapi - * /api/v1/products/{product_id}: - * delete: - * summary: Delete a product by its ID - * tags: [Product] - * parameters: - * - in: path - * name: product_id - * required: true - * schema: - * type: string - * description: The ID of the product to delete - * responses: - * 200: - * description: Product deleted successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Product deleted successfully - * status_code: - * type: integer - * example: 200 - * 400: - * description: Bad request due to invalid product ID - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: Bad Request - * message: - * type: string - * example: Invalid Product Id - * status_code: - * type: integer - * example: 400 - * 404: - * description: Product not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: Not Found - * message: - * type: string - * example: Product not found - * status_code: - * type: integer - * example: 404 - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: An unexpected error occurred - * message: - * type: string - * example: Internal server error - * status_code: - * type: integer - * example: 500 - */ - - public deleteProduct = async (req: Request, res: Response) => { - const { org_id, product_id } = req.params; - await this.productService.deleteProduct(org_id, product_id); - res.status(200).json({ message: "Product deleted successfully" }); - }; } export { ProductController }; diff --git a/src/routes/product.ts b/src/routes/product.ts index 37b0b8d1..3b9b7b40 100644 --- a/src/routes/product.ts +++ b/src/routes/product.ts @@ -17,19 +17,4 @@ productRouter.post( productController.createProduct, ); -productRouter.get( - "/organizations/:org_id/products/search", - authMiddleware, - validateUserToOrg, - productController.getProduct, -); - -productRouter.delete( - "/organizations/:org_id/products/:product_id", - authMiddleware, - adminOnly, - validateUserToOrg, - productController.deleteProduct, -); - export { productRouter }; diff --git a/src/services/product.services.ts b/src/services/product.services.ts index 0543b911..8ba5bb13 100644 --- a/src/services/product.services.ts +++ b/src/services/product.services.ts @@ -105,66 +105,4 @@ export class ProductService { }, }; } - - public async getProducts( - orgId: string, - query: { - name?: string; - category?: string; - minPrice?: number; - maxPrice?: number; - }, - page: number = 1, - limit: number = 10, - ) { - const org = await this.organizationRepository.findOne({ - where: { id: orgId }, - }); - if (!org) { - throw new ServerError( - "Unprocessable entity exception: Invalid organization credentials", - ); - } - - const { name, category, minPrice, maxPrice } = query; - const queryBuilder = this.productRepository - .createQueryBuilder("product") - .where("product.orgId = :orgId", { orgId }); - - if (name) { - queryBuilder.andWhere("product.name ILIKE :name", { name: `%${name}%` }); - } - if (minPrice) { - queryBuilder.andWhere("product.price >= :minPrice", { minPrice }); - } - if (maxPrice) { - queryBuilder.andWhere("product.price <= :maxPrice", { maxPrice }); - } - - const skip = (page - 1) * limit; - queryBuilder.skip(skip).take(limit); - - const [products, total] = await queryBuilder.getManyAndCount(); - - return { - success: true, - statusCode: 200, - data: { - products, - pagination: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }, - }; - } - public async deleteProduct(org_id: string, product_id: string) { - const entities = await this.checkEntities({ - organization: org_id, - product: product_id, - }); - return this.productRepository.remove(entities.product); - } } diff --git a/src/test/product.spec.ts b/src/test/product.spec.ts index 253e6760..a23c0d2a 100644 --- a/src/test/product.spec.ts +++ b/src/test/product.spec.ts @@ -134,121 +134,4 @@ describe("ProductService", () => { ).rejects.toThrow(ServerError); }); }); - describe("getProducts", () => { - it("should search products successfully", async () => { - const mockOrgId = "1"; - const mockQuery = { name: "Test", minPrice: 0, maxPrice: 100 }; - const mockOrg = { id: "1", name: "Test Organization" }; - const mockProducts = [{ id: "1", name: "Test Product", price: 50 }]; - const mockTotalCount = 2; - - const mockQueryBuilder = { - where: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - take: jest.fn().mockReturnThis(), - getManyAndCount: jest - .fn() - .mockResolvedValue([mockProducts, mockTotalCount]), - }; - - productRepository.createQueryBuilder = jest - .fn() - .mockReturnValue(mockQueryBuilder); - organizationRepository.findOne = jest.fn().mockResolvedValue(mockOrg); - - const result = await productService.getProducts(mockOrgId, mockQuery); - expect(result.success).toBe(true); - expect(result.statusCode).toBe(200); - expect(result.data.products).toEqual(mockProducts); - expect(result.data.pagination.total).toBe(mockTotalCount); - expect(result.data.pagination.page).toBe(1); - expect(result.data.pagination.limit).toBe(10); - }); - it("should search products successfully with specified pagination", async () => { - const mockOrgId = "1"; - const mockQuery = { name: "Test", minPrice: 0, maxPrice: 100 }; - const mockPage = 2; - const mockLimit = 5; - const mockOrg = { id: "1", name: "Test Organization" }; - const mockProducts = [{ id: "1", name: "Test Product", price: 50 }]; - const mockTotalCount = 5; - - const mockQueryBuilder = { - where: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - take: jest.fn().mockReturnThis(), - getManyAndCount: jest - .fn() - .mockResolvedValue([mockProducts, mockTotalCount]), - }; - - productRepository.createQueryBuilder = jest - .fn() - .mockReturnValue(mockQueryBuilder); - organizationRepository.findOne = jest.fn().mockResolvedValue(mockOrg); - - const result = await productService.getProducts( - mockOrgId, - mockQuery, - mockPage, - mockLimit, - ); - expect(result.success).toBe(true); - expect(result.statusCode).toBe(200); - expect(result.data.pagination.total).toBe(mockTotalCount); - expect(result.data.pagination.page).toBe(mockPage); - expect(result.data.pagination.limit).toBe(mockLimit); - }); - - it("should return an empty product array when product is not found", async () => { - const mockOrgId = "1"; - const mockQuery = { name: "Nonexistent Product" }; - const mockOrg = { id: "1", name: "Test Organization" }; - const mockTotalCount = 0; - const mockProducts = []; - - const mockQueryBuilder = { - where: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - take: jest.fn().mockReturnThis(), - getManyAndCount: jest.fn().mockResolvedValue([[], 0]), - }; - - productRepository.createQueryBuilder = jest - .fn() - .mockReturnValue(mockQueryBuilder); - organizationRepository.findOne = jest.fn().mockResolvedValue(mockOrg); - - const result = await productService.getProducts(mockOrgId, mockQuery); - expect(result.success).toBe(true); - expect(result.statusCode).toBe(200); - expect(result.data.pagination.total).toBe(mockTotalCount); - expect(result.data.products).toStrictEqual(mockProducts); - }); - - it("should throw a ServerError when organization is not found", async () => { - const mockOrgId = "nonexistentOrg"; - const mockQuery = { name: "Test Product" }; - - organizationRepository.findOne = jest.fn().mockResolvedValue(undefined); - - await expect( - productService.getProducts(mockOrgId, mockQuery), - ).rejects.toThrow(ServerError); - }); - - it("should throw a server error when organization is not found", async () => { - const mockOrgId = "nonexistentOrg"; - const mockQuery = { name: "Test Product" }; - - organizationRepository.findOne = jest.fn().mockResolvedValue(undefined); - - await expect( - productService.getProducts(mockOrgId, mockQuery), - ).rejects.toThrow(ServerError); - }); - }); }); From 86787bcf1c168d2a4a4f80fa5b4c13cd9e5f4644 Mon Sep 17 00:00:00 2001 From: Adeosun Oluwaseyi Date: Wed, 7 Aug 2024 23:32:06 +0100 Subject: [PATCH 2/2] feat: delete product --- src/controllers/ProductController.ts | 83 ++++++++++++++++++++++++++++ src/routes/product.ts | 8 +++ src/services/product.services.ts | 17 ++++++ src/test/product.spec.ts | 63 +++++++++++++++++++++ 4 files changed, 171 insertions(+) diff --git a/src/controllers/ProductController.ts b/src/controllers/ProductController.ts index 3ab67e43..7a119db0 100644 --- a/src/controllers/ProductController.ts +++ b/src/controllers/ProductController.ts @@ -105,6 +105,89 @@ class ProductController { .status(201) .json({ message: "Product created successfully", data: newProduct }); }; + + /** + * @openapi + * /api/v1/products/{product_id}: + * delete: + * summary: Delete a product by its ID + * tags: [Product] + * parameters: + * - in: path + * name: product_id + * required: true + * schema: + * type: string + * description: The ID of the product to delete + * responses: + * 200: + * description: Product deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Product deleted successfully + * status_code: + * type: integer + * example: 200 + * 400: + * description: Bad request due to invalid product ID + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: Bad Request + * message: + * type: string + * example: Invalid Product Id + * status_code: + * type: integer + * example: 400 + * 404: + * description: Product not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: Not Found + * message: + * type: string + * example: Product not found + * status_code: + * type: integer + * example: 404 + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: An unexpected error occurred + * message: + * type: string + * example: Internal server error + * status_code: + * type: integer + * example: 500 + */ + + public deleteProduct = async (req: Request, res: Response) => { + const { org_id, product_id } = req.params; + await this.productService.deleteProduct(org_id, product_id); + res.status(200).json({ message: "Product deleted successfully" }); + }; } export { ProductController }; diff --git a/src/routes/product.ts b/src/routes/product.ts index 3b9b7b40..199be293 100644 --- a/src/routes/product.ts +++ b/src/routes/product.ts @@ -17,4 +17,12 @@ productRouter.post( productController.createProduct, ); +productRouter.delete( + "/organizations/:org_id/products/:product_id", + authMiddleware, + adminOnly, + validateUserToOrg, + productController.deleteProduct, +); + export { productRouter }; diff --git a/src/services/product.services.ts b/src/services/product.services.ts index 8ba5bb13..328eeefa 100644 --- a/src/services/product.services.ts +++ b/src/services/product.services.ts @@ -105,4 +105,21 @@ export class ProductService { }, }; } + public async deleteProduct(org_id: string, product_id: string) { + try { + const entities = await this.checkEntities({ + organization: org_id, + product: product_id, + }); + + if (!entities.product) { + throw new Error("Product not found"); + } + + await this.productRepository.remove(entities.product); + return { message: "Product deleted successfully" }; + } catch (error) { + throw new Error(`Failed to delete product: ${error.message}`); + } + } } diff --git a/src/test/product.spec.ts b/src/test/product.spec.ts index a23c0d2a..e369ae37 100644 --- a/src/test/product.spec.ts +++ b/src/test/product.spec.ts @@ -17,6 +17,7 @@ jest.mock("../data-source", () => ({ create: jest.fn().mockReturnValue({}), save: jest.fn(), findOne: jest.fn(), + remove: jest.fn(), createQueryBuilder: jest.fn(() => ({ where: jest.fn().mockReturnThis(), andWhere: jest.fn().mockReturnThis(), @@ -134,4 +135,66 @@ describe("ProductService", () => { ).rejects.toThrow(ServerError); }); }); + describe("deleteProduct", () => { + it("should delete the product from the organization", async () => { + const org_id = "org123"; + const product_id = "prod123"; + // Mock data + const mockProduct = { id: product_id, name: "Test Product" } as Product; + productService["checkEntities"] = jest + .fn() + .mockResolvedValue({ product: mockProduct }); + productRepository.remove = jest.fn().mockResolvedValue(mockProduct); + + await productService.deleteProduct(org_id, product_id); + + // Verify that the checkEntities and remove methods were called with the correct parameters + expect(productService["checkEntities"]).toHaveBeenCalledWith({ + organization: org_id, + product: product_id, + }); + expect(productRepository.remove).toHaveBeenCalledWith(mockProduct); + }); + it("should throw an error if the product is not found", async () => { + const org_id = "org123"; + const product_id = "prod123"; + + // Mock the checkEntities method to return undefined for product + productService["checkEntities"] = jest + .fn() + .mockResolvedValue({ product: undefined }); + + await expect( + productService.deleteProduct(org_id, product_id), + ).rejects.toThrow("Product not found"); + + // Verify that the checkEntities method was called correctly and remove method was not called + expect(productService["checkEntities"]).toHaveBeenCalledWith({ + organization: org_id, + product: product_id, + }); + expect(productRepository.remove).not.toHaveBeenCalled(); + }); + + it("should throw an error if checkEntities fails", async () => { + const org_id = "org123"; + const product_id = "prod123"; + + // Mock the checkEntities method to throw an error + productService["checkEntities"] = jest + .fn() + .mockRejectedValue(new Error("Check entities failed")); + + await expect( + productService.deleteProduct(org_id, product_id), + ).rejects.toThrow("Failed to delete product: Check entities failed"); + + // Verify that the checkEntities method was called correctly and remove method was not called + expect(productService["checkEntities"]).toHaveBeenCalledWith({ + organization: org_id, + product: product_id, + }); + expect(productRepository.remove).not.toHaveBeenCalled(); + }); + }); });