From 12277c654ba82302aa8aaaa682b01328819da4dd Mon Sep 17 00:00:00 2001 From: KensiPatel Date: Fri, 6 Mar 2026 22:22:19 +0530 Subject: [PATCH 1/2] Implement comment feature skeleton --- .../db/repositories/comments_repository.ts | 34 ++++++ back_end/src/routes/comments.route.ts | 104 ++++++++++++++++++ back_end/src/services/commenstService.ts | 54 +++++++++ 3 files changed, 192 insertions(+) create mode 100644 back_end/src/db/repositories/comments_repository.ts create mode 100644 back_end/src/routes/comments.route.ts create mode 100644 back_end/src/services/commenstService.ts diff --git a/back_end/src/db/repositories/comments_repository.ts b/back_end/src/db/repositories/comments_repository.ts new file mode 100644 index 0000000..64bd906 --- /dev/null +++ b/back_end/src/db/repositories/comments_repository.ts @@ -0,0 +1,34 @@ +import { db } from "../db.client"; +import { comment } from "../schema/comment"; +import { eq } from "drizzle-orm"; + +export interface CreateCommentInput { + expenseId: number; + userId: number; + content: string; +} + +export async function createComment(data: CreateCommentInput): Promise { + await db().insert(comment).values({ + expenseId: data.expenseId, + userId: data.userId, + content: data.content, + }); +} + +export async function getCommentsByExpenseId(expenseId: number): Promise< + { + id: number; + expenseId: number; + userId: number; + content: string; + createdAt: Date; + }[] +> { + const result = await db() + .select() + .from(comment) + .where(eq(comment.expenseId, expenseId)); + + return result; +} \ No newline at end of file diff --git a/back_end/src/routes/comments.route.ts b/back_end/src/routes/comments.route.ts new file mode 100644 index 0000000..d7cf6f5 --- /dev/null +++ b/back_end/src/routes/comments.route.ts @@ -0,0 +1,104 @@ +import { Router, type Response } from "express"; +import { authMiddleware } from "../middleware/auth-middleware"; +import { z } from "zod"; +import { addCommentService, getCommentsService } from "src/services/commentsService"; + +const router = Router(); +const commentBodySchema = z.object({ + content: z.string().min(1, "Comment cannot be empty").max(500), +}); + +const expenseParamSchema = z.object({ + id: z + .string() + .transform((val) => Number(val)) + .refine((val) => Number.isInteger(val) && val > 0, { + message: "Expense id must be a positive integer", + }), +}); + +router.post("/expenses/:id/comments",authMiddleware(),async (req, res: Response): Promise => { + try { + const parsedParams = expenseParamSchema.safeParse(req.params); + if (!parsedParams.success) { + res.status(400).json({ message: "Invalid expense id" }); + return; + } + + const parsedBody = commentBodySchema.safeParse(req.body); + if (!parsedBody.success) { + res.status(400).json({ message: "Invalid input" }); + return; + } + + const expenseId = parsedParams.data.id; + const { content } = parsedBody.data; + + await addCommentService( + expenseId, + req.user.id, + req.user.role, + content + ); + + res.status(201).json({ message: "Comment added successfully" }); + } + catch (error: unknown) { + if (error instanceof Error) { + if (error.message === "Expense not found") { + res.status(404).json({ message: error.message }); + return; + } + + if (error.message === "Access denied") { + res.status(403).json({ message: error.message }); + return; + } + + if (error.message === "Cannot comment on draft expense") { + res.status(400).json({ message: error.message }); + return; + } + + return; + } + + res.status(500).json({ message: "Internal server error" }); + } +}); + +router.get("/expenses/:id/comments",authMiddleware(),async (req, res: Response): Promise => { + try { + const parsedParams = expenseParamSchema.safeParse(req.params); + if (!parsedParams.success) { + res.status(400).json({ message: "Invalid expense id" }); + return; + } + + const expenseId = parsedParams.data.id; + const result = await getCommentsService( + expenseId, + req.user.id, + req.user.role, + ); + + res.status(200).json({ data: result }); + } + catch (error: unknown) { + if (error instanceof Error) { + if (error.message === "Expense not found") { + res.status(404).json({ message: error.message }); + return; + } + if (error.message === "Access denied") { + res.status(403).json({ message: error.message }); + return; + } + res.status(400).json({ message: error.message }); + return; + } + res.status(500).json({ message: "Internal server error" }); + } +}); + +export default router; \ No newline at end of file diff --git a/back_end/src/services/commenstService.ts b/back_end/src/services/commenstService.ts new file mode 100644 index 0000000..f654396 --- /dev/null +++ b/back_end/src/services/commenstService.ts @@ -0,0 +1,54 @@ +import { getCommentsByExpenseId,createComment } from "src/db/repositories/comments_repository"; +import { findExpenseById } from "../services/expense.service"; +import { role } from "../types/user.type"; +import type { Role } from "../types/user.type"; + +export async function addCommentService( + expenseId: number, + userId: number, + userRole: Role, + content: string +): Promise { + const expense = await findExpenseById(expenseId); + + if (!expense) {throw new Error("Expense not found");} + + if (userRole === role.employee) { + if (expense.userId !== userId) {throw new Error("Access denied");} + } + + if (expense.status === "draft") {throw new Error("Cannot comment on draft expense");} + + await createComment({ + expenseId, + userId, + content, + }); + + return; +} + +export async function getCommentsService( + expenseId: number, + userId: number, + userRole: Role +): Promise< + { + id: number; + expenseId: number; + userId: number; + content: string; + createdAt: Date; + }[] +> { + + const expense = await findExpenseById(expenseId); + + if (!expense) {throw new Error("Expense not found");} + + if (userRole === role.employee) { + if (expense.userId !== userId) {throw new Error("Access denied");} + } + + return await getCommentsByExpenseId(expenseId); +} \ No newline at end of file From ddd7d5386ced863bbff4254f237946c4d589411f Mon Sep 17 00:00:00 2001 From: KensiPatel Date: Sat, 7 Mar 2026 10:13:52 +0530 Subject: [PATCH 2/2] order comments by createdAt ASC in getCommentsByExpenseId --- back_end/src/db/repositories/comments_repository.ts | 5 +++-- back_end/src/routes/comments.route.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/back_end/src/db/repositories/comments_repository.ts b/back_end/src/db/repositories/comments_repository.ts index 64bd906..5443d7d 100644 --- a/back_end/src/db/repositories/comments_repository.ts +++ b/back_end/src/db/repositories/comments_repository.ts @@ -1,6 +1,6 @@ import { db } from "../db.client"; import { comment } from "../schema/comment"; -import { eq } from "drizzle-orm"; +import { eq, asc } from "drizzle-orm"; export interface CreateCommentInput { expenseId: number; @@ -28,7 +28,8 @@ export async function getCommentsByExpenseId(expenseId: number): Promise< const result = await db() .select() .from(comment) - .where(eq(comment.expenseId, expenseId)); + .where(eq(comment.expenseId, expenseId)) + .orderBy(asc(comment.createdAt)); return result; } \ No newline at end of file diff --git a/back_end/src/routes/comments.route.ts b/back_end/src/routes/comments.route.ts index d7cf6f5..feddadb 100644 --- a/back_end/src/routes/comments.route.ts +++ b/back_end/src/routes/comments.route.ts @@ -1,7 +1,7 @@ import { Router, type Response } from "express"; import { authMiddleware } from "../middleware/auth-middleware"; import { z } from "zod"; -import { addCommentService, getCommentsService } from "src/services/commentsService"; +import { addCommentService, getCommentsService } from "src/services/commenstService"; const router = Router(); const commentBodySchema = z.object({