Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update product #584

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion contributors.md
Original file line number Diff line number Diff line change
@@ -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] (<https://github.com/myconpeter>)
[Adetayo Adewobi] (<https://github.com/livingHopeDev>)
7 changes: 7 additions & 0 deletions q
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions src/controllers/ProductController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
2 changes: 1 addition & 1 deletion src/models/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
9 changes: 6 additions & 3 deletions src/routes/product.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -37,5 +36,9 @@ productRouter.get(
validOrgAdmin,
productController.getSingleProduct,
);

productRouter.patch(
"/organisations/:orgId/products/:productId",
authMiddleware,
productController.updateProduct,
);
export { productRouter };
65 changes: 65 additions & 0 deletions src/services/product.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Product>;
private organizationRepository: Repository<Organization>;
private userRepository: Repository<User>;

private entities: {
[key: string]: {
Expand All @@ -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: {
Expand Down Expand Up @@ -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,
},
};
}
}
2 changes: 2 additions & 0 deletions src/test/product.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ describe("ProductService", () => {
let productService: ProductService;
let productRepository: Repository<Product>;
let organizationRepository: Repository<Organization>;
let userRepository: Repository<User>;

beforeEach(() => {
productService = new ProductService();
productRepository = productService["productRepository"];
organizationRepository = productService["organizationRepository"];
userRepository = productService["userRepository"];
});

describe("createProducts", () => {
Expand Down
168 changes: 168 additions & 0 deletions src/test/updateProduct.spec.ts
Original file line number Diff line number Diff line change
@@ -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<Repository<Product>>;
let userRepository: jest.Mocked<Repository<User>>;
let organizationRepository: jest.Mocked<Repository<Organization>>;

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);
});
});
});
Loading