feat(expenses): allow editing paid expenses (status, amountPayed, paymentDate)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user