-
Notifications
You must be signed in to change notification settings - Fork 2
feat: implement expense creation functionality with validation #24
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
base: main
Are you sure you want to change the base?
Changes from 9 commits
cb171e9
df1f6c4
5ac4a58
518e384
3dd4452
7c53317
8a1fa27
47dfb7c
dbb9558
e5279fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { db } from "../db.client"; | ||
| import { expense } from "../schema/expense"; | ||
| import { eq } from "drizzle-orm"; | ||
| import { CreateExpenseInput } from "src/types/expense.type"; | ||
|
|
||
| export const createExpense = async ({ | ||
| title, | ||
| description, | ||
| amount, | ||
| userId, | ||
| }: CreateExpenseInput) => { | ||
| await db().insert(expense).values({ | ||
| title, | ||
| description, | ||
| amount, | ||
| userId, | ||
| status: "draft", | ||
| }); | ||
|
|
||
| return { msg: "status: draft" }; | ||
| }; | ||
|
|
||
| export const getExpenseById = async (id: number) => { | ||
| const result = await db() | ||
| .select({ | ||
| id: expense.id, | ||
| userId: expense.userId, | ||
| title: expense.title, | ||
| amount: expense.amount, | ||
| status: expense.status, | ||
| }) | ||
| .from(expense) | ||
| .where(eq(expense.id, id)); | ||
|
|
||
| return result[0]; | ||
| }; | ||
|
Comment on lines
+23
to
+36
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return promise |
||
|
|
||
| export const updateExpenseStatus = async (id: number, status: "draft" | "submitted" | "accepted" | "rejected") => { | ||
| await db() | ||
| .update(expense) | ||
| .set({ status, | ||
| updatedAt: new Date() }) | ||
| .where(eq(expense.id, id)); | ||
| }; | ||
|
Comment on lines
+38
to
+44
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return promise |
||
|
|
||
|
|
||
| export const updateExpense = async ( | ||
| id: number, | ||
| data: Partial<Pick<CreateExpenseInput, "title" | "amount" | "description">> | ||
| ) => { | ||
| await db() | ||
| .update(expense) | ||
| .set({ ...data, updatedAt: new Date() }) | ||
| .where(eq(expense.id, id)); | ||
| }; | ||
|
|
||
| export const deleteExpense = async (id: number) => { | ||
| await db() | ||
| .delete(expense) | ||
| .where(eq(expense.id, id)); | ||
| }; | ||
|
|
||
| export const getExpensesByUserId = async (userId: number) => { | ||
| const result = await db() | ||
| .select() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do not select * |
||
| .from(expense) | ||
| .where(eq(expense.userId, userId)); | ||
|
|
||
| return result; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| import { Router, type Response } from "express"; | ||
| import { authMiddleware } from "../middleware/auth-middleware"; | ||
| import { z } from "zod"; | ||
| import { createExpenseService, getExpensesByUserIdService } from "../services/expenseService"; | ||
| import { submitExpenseService } from "../services/expenseService"; | ||
| import { updateExpenseService } from "../services/expenseService"; | ||
| import { deleteExpenseService } from "../services/expenseService"; | ||
|
|
||
| const router = Router(); | ||
|
|
||
| const expenseSchema = z.object({ | ||
| title: z.string().trim().min(1, "Title should not be empty").max(255), | ||
| amount: z | ||
| .any() | ||
| .refine( | ||
| (value) => value !== "" && value !== null && value !== undefined, | ||
| "Amount should not be empty" | ||
| ) | ||
| .transform((value) => Number(value)) | ||
| .refine((value) => !Number.isNaN(value), "Amount must be a number") | ||
| .refine((value) => value > 0, "Amount must be greater than zero"), | ||
| description: z.string().min(2).max(255), | ||
| createdBy: z.number().optional(), | ||
| }); | ||
|
|
||
| const updateExpenseSchema = z | ||
| .object({ | ||
| title: z.string().trim().min(1, "Title should not be empty").max(255).optional(), | ||
| amount: z | ||
| .any() | ||
| .optional() | ||
| .refine( | ||
| (value) => | ||
| value === undefined || | ||
| (value !== "" && value !== null && value !== undefined), | ||
| "Amount should not be empty" | ||
| ) | ||
| .transform((value) => (value === undefined ? undefined : Number(value))) | ||
| .refine( | ||
| (value) => value === undefined || !Number.isNaN(value), | ||
| "Amount must be a number" | ||
| ) | ||
| .refine( | ||
| (value) => value === undefined || value > 0, | ||
| "Amount must be greater than zero" | ||
| ), | ||
| description: z.string().trim().min(2).max(255).optional(), | ||
| }) | ||
| .refine((data) => Object.keys(data).length > 0, { | ||
| message: "Provide at least one field to update", | ||
| }); | ||
|
|
||
| router.post("/draft", authMiddleware(), async (req, res: Response) => { | ||
| try { | ||
| const parsed = expenseSchema.safeParse(req.body); | ||
|
|
||
| if (!parsed.success) { | ||
| return res.status(400).json({ | ||
| message: parsed.error.issues[0]?.message ?? "Invalid input", | ||
| }); | ||
| } | ||
|
|
||
| const { title, amount, description} = parsed.data; | ||
| const userId = req.user.id; | ||
| const result = await createExpenseService({ | ||
| title, | ||
| amount, | ||
| description, | ||
| userId, | ||
| }); | ||
|
|
||
| return res.status(201).json(result); | ||
| } catch (err: any) { | ||
| if (err instanceof z.ZodError) { | ||
| return res.status(400).json({ | ||
| message: "Invalid input", | ||
| }); | ||
| } else if (err.message.includes("Amount must be greater than zero")) { | ||
| return res.status(400).json({ | ||
| message: "Amount must be greater than zero", | ||
| }); | ||
| } | ||
| return res.status(500).json({ message: "Failed to create expense" }); | ||
| } | ||
| }); | ||
|
|
||
| router.patch("/submit/:id", authMiddleware(), async (req, res: Response) => { | ||
| try { | ||
| const parsedId = z.coerce.number().int().positive().safeParse(req.params.id); | ||
|
|
||
| if (!parsedId.success) { | ||
| return res.status(400).json({ | ||
| message: "Invalid input", | ||
| }); | ||
| } | ||
|
|
||
| const id = parsedId.data; | ||
| const userId = req.user.id; | ||
| await submitExpenseService(id, userId); | ||
|
|
||
| return res.status(200).json({ message: "Expense submitted successfully" }); | ||
| } catch (err: any) { | ||
| if (err instanceof z.ZodError) { | ||
| return res.status(400).json({ | ||
| message: "Invalid input", | ||
| }); | ||
| } else if (err.message.includes("Expense not found")) { | ||
| return res.status(404).json({ | ||
| message: "Expense not found", | ||
| }); | ||
| } else if (err.message.includes("Unauthorized to submit this expense")) { | ||
| return res.status(403).json({ | ||
| message: "Unauthorized to submit this expense", | ||
| }); | ||
| } else if (err.message.includes("Only draft expenses can be submitted")) { | ||
| return res.status(400).json({ | ||
| message: "Only draft expenses can be submitted", | ||
| }); | ||
| } | ||
| return res.status(500).json({ message: "Failed to submit expense" }); | ||
| } | ||
| }); | ||
|
|
||
| router.patch("/update/:id", authMiddleware(), async (req, res: Response) => { | ||
| try { | ||
| const parsedId = z.coerce.number().int().positive().safeParse(req.params.id); | ||
| const parsedBody = updateExpenseSchema.safeParse(req.body); | ||
|
|
||
| if (!parsedId.success) { | ||
| return res.status(400).json({ | ||
| message: "Invalid input", | ||
| }); | ||
| } | ||
|
|
||
| if (!parsedBody.success) { | ||
| return res.status(400).json({ | ||
| message: parsedBody.error.issues[0]?.message ?? "Invalid input", | ||
| }); | ||
| } | ||
|
|
||
| const id = parsedId.data; | ||
| const userId = req.user.id; | ||
| const data = parsedBody.data; | ||
| await updateExpenseService(id, userId, data); | ||
|
|
||
| return res.status(200).json({ message: "Expense updated successfully" }); | ||
| } catch (err: any) { | ||
| if (err instanceof z.ZodError) { | ||
| return res.status(400).json({ | ||
| message: "Invalid input", | ||
| }); | ||
| } else if (err.message.includes("Expense not found")) { | ||
| return res.status(404).json({ | ||
| message: "Expense not found", | ||
| }); | ||
| } else if (err.message.includes("Unauthorized to update this expense")) { | ||
| return res.status(403).json({ | ||
| message: "Unauthorized to update this expense", | ||
| }); | ||
| } else if (err.message.includes("Only draft expenses can be updated")) { | ||
| return res.status(400).json({ | ||
| message: "Only draft expenses can be updated", | ||
| }); | ||
| } else if (err.message.includes("Title and amount are same as last")) { | ||
| return res.status(400).json({ | ||
| message: "Title and amount are same as last", | ||
| }); | ||
| } else if (err.message.includes("Amount must be greater than zero")) { | ||
| return res.status(400).json({ | ||
| message: "Amount must be greater than zero", | ||
| }); | ||
| } | ||
| return res.status(500).json({ message: "Failed to update expense" }); | ||
| } | ||
| }); | ||
|
|
||
| router.delete("/delete/:id", authMiddleware(), async (req, res: Response) => { | ||
| try { | ||
| const parsedId = z.coerce.number().int().positive().safeParse(req.params.id); | ||
|
|
||
| if (!parsedId.success) { | ||
| return res.status(400).json({ | ||
| message: "Invalid input", | ||
| }); | ||
| } | ||
|
|
||
| const id = parsedId.data; | ||
| const userId = req.user.id; | ||
| await deleteExpenseService(id, userId); | ||
|
|
||
| return res.status(200).json({ message: "Expense deleted successfully" }); | ||
| } catch (err: any) { | ||
| if (err instanceof z.ZodError) { | ||
| return res.status(400).json({ | ||
| message: "Invalid input", | ||
| }); | ||
| } else if (err.message.includes("Expense not found")) { | ||
| return res.status(404).json({ | ||
| message: "Expense not found", | ||
| }); | ||
| } else if (err.message.includes("Unauthorized to delete this expense")) { | ||
| return res.status(403).json({ | ||
| message: "Unauthorized to delete this expense", | ||
| }); | ||
| } else if (err.message.includes("Only draft expenses can be deleted")) { | ||
| return res.status(400).json({ | ||
| message: "Only draft expenses can be deleted", | ||
| }); | ||
| } | ||
| return res.status(500).json({ message: "Failed to delete expense" }); | ||
| } | ||
| }); | ||
|
|
||
| router.get("/", authMiddleware(), async (req, res: Response) => { | ||
| try { | ||
| const userId = req.user.id; | ||
| const expenses = await getExpensesByUserIdService(userId); | ||
| return res.status(200).json({ expenses }); | ||
| } catch (err: any) { | ||
| return res.status(500).json({ message: "Failed to fetch expenses" }); | ||
| } | ||
| }); | ||
|
|
||
| export default router; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return promise