feat(expenses): allow editing paid expenses (status, amountPayed, paymentDate)

This commit is contained in:
Jose Selesan
2026-06-08 08:12:46 -03:00
parent 6f06ee18da
commit b08399908b
5 changed files with 273 additions and 43 deletions

View File

@@ -28,6 +28,15 @@ export const updateExpenseSchema = z.object({
dueDate: z
.string()
.datetime({ message: "La fecha debe ser una fecha ISO válida" }),
status: z.enum(["PENDING"]).optional(),
amountPayed: z
.number()
.positive("El monto pagado debe ser mayor a 0")
.optional(),
paymentDate: z
.string()
.datetime({ message: "La fecha debe ser una fecha ISO válida" })
.optional(),
});
function lastDayOfMonth(year: number, month: number): number {
@@ -199,22 +208,34 @@ export async function updateExpense(
data: z.infer<typeof updateExpenseSchema>,
) {
const existing = await prisma.expense.findUniqueOrThrow({ where: { id } });
if (existing.status !== PaymentStatus.PENDING) {
throw new Error("Solo se pueden editar gastos pendientes");
}
const dueDate = new Date(data.dueDate);
const year = dueDate.getUTCFullYear();
const month = dueDate.getUTCMonth() + 1;
const updateData: Record<string, unknown> = {
amount: data.amount,
dueDate,
year,
month,
};
if (data.status === "PENDING") {
updateData.status = PaymentStatus.PENDING;
updateData.amountPayed = null;
updateData.paymentDate = null;
} else if (existing.status === PaymentStatus.PAYED) {
if (data.amountPayed !== undefined) {
updateData.amountPayed = data.amountPayed;
}
if (data.paymentDate !== undefined) {
updateData.paymentDate = new Date(data.paymentDate);
}
}
const result = await prisma.expense.update({
where: { id },
data: {
amount: data.amount,
dueDate,
year,
month,
},
data: updateData,
});
cache.invalidateByPrefix("expenses:");
return result;

View File

@@ -471,6 +471,8 @@ describe("updateExpense", () => {
dueDate: new Date("2026-06-15T00:00:00.000Z"),
year: 2026,
month: 6,
amountPayed: null,
paymentDate: null,
};
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
mockPrisma.expense.update.mockImplementationOnce(
@@ -486,18 +488,82 @@ describe("updateExpense", () => {
expect(result.month).toBe(7);
});
test("rejects update for PAYED expense", async () => {
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce({
test("allows updating a PAYED expense", async () => {
const existing = {
id: 1,
status: PaymentStatus.PAYED,
amount: 1500,
amountPayed: 1500,
dueDate: new Date("2026-06-15T00:00:00.000Z"),
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
year: 2026,
month: 6,
};
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
mockPrisma.expense.update.mockImplementationOnce(
async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
);
const result = await updateExpense(1, {
amount: 2000,
dueDate: "2026-07-20T00:00:00.000Z",
amountPayed: 1800,
});
expect(result.amount).toBe(2000);
expect(result.year).toBe(2026);
expect(result.month).toBe(7);
expect(result.amountPayed).toBe(1800);
});
test("reverting a PAYED expense to PENDING clears payment info", async () => {
const existing = {
id: 1,
status: PaymentStatus.PAYED,
amount: 1500,
amountPayed: 1500,
dueDate: new Date("2026-06-15T00:00:00.000Z"),
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
year: 2026,
month: 6,
};
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
mockPrisma.expense.update.mockImplementationOnce(
async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
);
const result = await updateExpense(1, {
amount: 1500,
dueDate: "2026-06-15T00:00:00.000Z",
status: "PENDING",
});
expect(
updateExpense(1, {
amount: 100,
dueDate: "2026-07-15T00:00:00.000Z",
}),
).rejects.toThrow("Solo se pueden editar gastos pendientes");
expect(result.status).toBe(PaymentStatus.PENDING);
});
test("sets amountPayed and paymentDate to null when reverting to PENDING", async () => {
const existing = {
id: 1,
status: PaymentStatus.PAYED,
amount: 1500,
amountPayed: 1500,
dueDate: new Date("2026-06-15T00:00:00.000Z"),
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
year: 2026,
month: 6,
};
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
mockPrisma.expense.update.mockImplementationOnce(
async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
);
const result = await updateExpense(1, {
amount: 1500,
dueDate: "2026-06-15T00:00:00.000Z",
status: "PENDING",
});
expect(result.amountPayed).toBeNull();
expect(result.paymentDate).toBeNull();
});
});