feat(expenses): enhance expenses management with filtering and search capabilities

This commit is contained in:
Jose Selesan
2026-05-29 14:56:35 -03:00
parent e99e97aa14
commit d3cf386be5
9 changed files with 379 additions and 17 deletions

View File

@@ -88,12 +88,20 @@ export async function listExpenses(params: {
status?: string;
page?: number;
pageSize?: number;
periodicExpenseId?: number;
search?: string;
}) {
const { status, page = 1, pageSize = 10 } = params;
const { status, page = 1, pageSize = 10, periodicExpenseId, search } = params;
const where: Record<string, unknown> = {};
if (status === "PENDING" || status === "PAYED") {
where.status = status;
}
if (periodicExpenseId !== undefined) {
where.periodicExpenseId = periodicExpenseId;
}
if (search) {
where.description = { contains: search, mode: "insensitive" };
}
const [data, total] = await Promise.all([
prisma.expense.findMany({
where,

View File

@@ -5,6 +5,14 @@ export async function listExpensesHandler(c: Context) {
const status = c.req.query("status");
const page = parseInt(c.req.query("page") ?? "1", 10);
const pageSize = parseInt(c.req.query("pageSize") ?? "10", 10);
const result = await listExpenses({ status, page, pageSize });
const periodicExpenseId = c.req.query("periodicExpenseId");
const search = c.req.query("search");
const result = await listExpenses({
status,
page,
pageSize,
periodicExpenseId: periodicExpenseId ? parseInt(periodicExpenseId, 10) : undefined,
search: search || undefined,
});
return c.json(result);
}

View File

@@ -13,6 +13,7 @@ const mockPrisma = {
findFirst: mock(),
create: mock(),
update: mock(),
count: mock(),
},
};
@@ -26,6 +27,7 @@ beforeEach(() => {
fn.mockClear();
}
}
mockPrisma.expense.count.mockResolvedValue(0);
});
const {
@@ -229,7 +231,7 @@ describe("listPeriodicExpenses", () => {
expect(result).toEqual(mockItems);
expect(mockPrisma.periodicExpense.findMany).toHaveBeenCalledWith({
where: { isDeleted: false },
orderBy: { createdAt: "desc" },
orderBy: { description: "asc" },
});
});
});
@@ -289,21 +291,65 @@ describe("softDeletePeriodicExpense", () => {
describe("listExpenses", () => {
test("returns all expenses when no status filter", async () => {
mockPrisma.expense.findMany.mockResolvedValueOnce([]);
await listExpenses();
await listExpenses({});
expect(mockPrisma.expense.findMany).toHaveBeenCalledWith({
where: {},
include: { periodicExpense: true },
orderBy: { dueDate: "asc" },
orderBy: { dueDate: "desc" },
skip: 0,
take: 10,
});
});
test("filters by status when provided", async () => {
mockPrisma.expense.findMany.mockResolvedValueOnce([]);
await listExpenses("PENDING");
await listExpenses({ status: "PENDING" });
expect(mockPrisma.expense.findMany).toHaveBeenCalledWith({
where: { status: "PENDING" },
include: { periodicExpense: true },
orderBy: { dueDate: "asc" },
orderBy: { dueDate: "desc" },
skip: 0,
take: 10,
});
});
test("filters by periodicExpenseId when provided", async () => {
mockPrisma.expense.findMany.mockResolvedValueOnce([]);
await listExpenses({ periodicExpenseId: 3 });
expect(mockPrisma.expense.findMany).toHaveBeenCalledWith({
where: { periodicExpenseId: 3 },
include: { periodicExpense: true },
orderBy: { dueDate: "desc" },
skip: 0,
take: 10,
});
});
test("filters by search query with case-insensitive contains", async () => {
mockPrisma.expense.findMany.mockResolvedValueOnce([]);
await listExpenses({ search: "Gim" });
expect(mockPrisma.expense.findMany).toHaveBeenCalledWith({
where: { description: { contains: "Gim", mode: "insensitive" } },
include: { periodicExpense: true },
orderBy: { dueDate: "desc" },
skip: 0,
take: 10,
});
});
test("combines status, periodicExpenseId, and search filters", async () => {
mockPrisma.expense.findMany.mockResolvedValueOnce([]);
await listExpenses({ status: "PENDING", periodicExpenseId: 1, search: "Agua" });
expect(mockPrisma.expense.findMany).toHaveBeenCalledWith({
where: {
status: "PENDING",
periodicExpenseId: 1,
description: { contains: "Agua", mode: "insensitive" },
},
include: { periodicExpense: true },
orderBy: { dueDate: "desc" },
skip: 0,
take: 10,
});
});
});