|
|
|
|
@@ -1,7 +1,15 @@
|
|
|
|
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
|
|
import { PaymentStatus } from "../src/generated/prisma/client";
|
|
|
|
|
|
|
|
|
|
const mockWalletFindUnique = mock();
|
|
|
|
|
const mockWalletUpdate = mock();
|
|
|
|
|
const mockMovementCreate = mock();
|
|
|
|
|
const mockTransaction = mock(async (operations: Array<Promise<unknown>>) =>
|
|
|
|
|
Promise.all(operations),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const mockPrisma = {
|
|
|
|
|
$transaction: mockTransaction,
|
|
|
|
|
periodicExpense: {
|
|
|
|
|
findMany: mock(),
|
|
|
|
|
findUniqueOrThrow: mock(),
|
|
|
|
|
@@ -16,6 +24,13 @@ const mockPrisma = {
|
|
|
|
|
update: mock(),
|
|
|
|
|
count: mock(),
|
|
|
|
|
},
|
|
|
|
|
wallet: {
|
|
|
|
|
findUnique: mockWalletFindUnique,
|
|
|
|
|
update: mockWalletUpdate,
|
|
|
|
|
},
|
|
|
|
|
walletMovement: {
|
|
|
|
|
create: mockMovementCreate,
|
|
|
|
|
},
|
|
|
|
|
quote: {
|
|
|
|
|
findFirst: mock(),
|
|
|
|
|
},
|
|
|
|
|
@@ -30,6 +45,7 @@ mock.module("../src/lib/prisma", () => ({
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
for (const model of Object.values(mockPrisma)) {
|
|
|
|
|
if (typeof model === "function") continue;
|
|
|
|
|
for (const fn of Object.values(
|
|
|
|
|
model as Record<string, ReturnType<typeof mock>>,
|
|
|
|
|
)) {
|
|
|
|
|
@@ -155,6 +171,7 @@ describe("schemas", () => {
|
|
|
|
|
description: "Ropa",
|
|
|
|
|
amount: 2500,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
walletId: 1,
|
|
|
|
|
});
|
|
|
|
|
expect(result.description).toBe("Ropa");
|
|
|
|
|
});
|
|
|
|
|
@@ -175,12 +192,16 @@ describe("schemas", () => {
|
|
|
|
|
const result = payExpenseSchema.parse({
|
|
|
|
|
amountPayed: 1400,
|
|
|
|
|
paymentDate: "2026-06-10T00:00:00.000Z",
|
|
|
|
|
walletId: 1,
|
|
|
|
|
});
|
|
|
|
|
expect(result.amountPayed).toBe(1400);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("allows missing paymentDate", () => {
|
|
|
|
|
const result = payExpenseSchema.parse({ amountPayed: 1400 });
|
|
|
|
|
const result = payExpenseSchema.parse({
|
|
|
|
|
amountPayed: 1400,
|
|
|
|
|
walletId: 1,
|
|
|
|
|
});
|
|
|
|
|
expect(result.amountPayed).toBe(1400);
|
|
|
|
|
expect(result.paymentDate).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
@@ -407,6 +428,7 @@ describe("createNonPeriodicExpense", () => {
|
|
|
|
|
description: "Ropa",
|
|
|
|
|
amount: 2500,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
walletId: 1,
|
|
|
|
|
};
|
|
|
|
|
const created = {
|
|
|
|
|
id: 1,
|
|
|
|
|
@@ -417,7 +439,11 @@ describe("createNonPeriodicExpense", () => {
|
|
|
|
|
year: 2026,
|
|
|
|
|
month: 6,
|
|
|
|
|
};
|
|
|
|
|
const wallet = { id: 1, balance: 10000 };
|
|
|
|
|
mockPrisma.expense.create.mockResolvedValueOnce(created);
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 7500 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await createNonPeriodicExpense(input);
|
|
|
|
|
expect(result.status).toBe(PaymentStatus.PAYED);
|
|
|
|
|
@@ -436,23 +462,48 @@ describe("createNonPeriodicExpense", () => {
|
|
|
|
|
|
|
|
|
|
describe("payExpense", () => {
|
|
|
|
|
test("updates status to PAYED with amountPayed and paymentDate", async () => {
|
|
|
|
|
const expense = {
|
|
|
|
|
id: 1,
|
|
|
|
|
description: "Ropa",
|
|
|
|
|
status: PaymentStatus.PENDING,
|
|
|
|
|
amount: 1400,
|
|
|
|
|
amountPayed: null,
|
|
|
|
|
paymentDate: null,
|
|
|
|
|
};
|
|
|
|
|
const paidExpense = {
|
|
|
|
|
id: 1,
|
|
|
|
|
status: PaymentStatus.PAYED,
|
|
|
|
|
amountPayed: 1400,
|
|
|
|
|
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
|
|
|
|
|
};
|
|
|
|
|
const wallet = { id: 1, balance: 10000, currency: "ARS" };
|
|
|
|
|
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(expense);
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockPrisma.expense.update.mockResolvedValueOnce(paidExpense);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 8600 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await payExpense(1, {
|
|
|
|
|
amountPayed: 1400,
|
|
|
|
|
paymentDate: "2026-06-10T00:00:00.000Z",
|
|
|
|
|
walletId: 1,
|
|
|
|
|
});
|
|
|
|
|
expect(result.status).toBe(PaymentStatus.PAYED);
|
|
|
|
|
expect(result.amountPayed).toBe(1400);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("defaults paymentDate to now when not provided", async () => {
|
|
|
|
|
const expense = {
|
|
|
|
|
id: 1,
|
|
|
|
|
description: "Ropa",
|
|
|
|
|
status: PaymentStatus.PENDING,
|
|
|
|
|
amount: 1500,
|
|
|
|
|
amountPayed: null,
|
|
|
|
|
paymentDate: null,
|
|
|
|
|
};
|
|
|
|
|
const wallet = { id: 1, balance: 10000, currency: "ARS" };
|
|
|
|
|
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(expense);
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockPrisma.expense.update.mockImplementationOnce(
|
|
|
|
|
async ({ where, data }) => ({
|
|
|
|
|
id: where.id,
|
|
|
|
|
@@ -462,8 +513,10 @@ describe("payExpense", () => {
|
|
|
|
|
paymentDate: data.paymentDate,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 8500 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await payExpense(1, { amountPayed: 1500 });
|
|
|
|
|
const result = await payExpense(1, { amountPayed: 1500, walletId: 1 });
|
|
|
|
|
expect(result.status).toBe(PaymentStatus.PAYED);
|
|
|
|
|
expect(result.paymentDate).toBeInstanceOf(Date);
|
|
|
|
|
});
|
|
|
|
|
@@ -739,7 +792,18 @@ describe("getBeloSellPriceForDate", () => {
|
|
|
|
|
|
|
|
|
|
describe("payExpense with BELO", () => {
|
|
|
|
|
test("sets beloPrice and usdcEquivalent when BELO quote is available", async () => {
|
|
|
|
|
const expense = {
|
|
|
|
|
id: 1,
|
|
|
|
|
description: "Ropa",
|
|
|
|
|
status: PaymentStatus.PENDING,
|
|
|
|
|
amount: 15000,
|
|
|
|
|
amountPayed: null,
|
|
|
|
|
paymentDate: null,
|
|
|
|
|
};
|
|
|
|
|
const paymentDate = new Date("2026-06-10T00:00:00.000Z");
|
|
|
|
|
const wallet = { id: 1, balance: 100000, currency: "ARS" };
|
|
|
|
|
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(expense);
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockPrisma.quoteHistory.findFirst
|
|
|
|
|
.mockResolvedValueOnce({
|
|
|
|
|
sell: "1500.00",
|
|
|
|
|
@@ -757,10 +821,13 @@ describe("payExpense with BELO", () => {
|
|
|
|
|
usdcEquivalent: data.usdcEquivalent,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 85000 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await payExpense(1, {
|
|
|
|
|
amountPayed: 15000,
|
|
|
|
|
paymentDate: "2026-06-10T00:00:00.000Z",
|
|
|
|
|
walletId: 1,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.beloPrice).toBe(1500);
|
|
|
|
|
@@ -768,6 +835,17 @@ describe("payExpense with BELO", () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("does not set BELO fields when no quote is available", async () => {
|
|
|
|
|
const expense = {
|
|
|
|
|
id: 1,
|
|
|
|
|
description: "Ropa",
|
|
|
|
|
status: PaymentStatus.PENDING,
|
|
|
|
|
amount: 15000,
|
|
|
|
|
amountPayed: null,
|
|
|
|
|
paymentDate: null,
|
|
|
|
|
};
|
|
|
|
|
const wallet = { id: 1, balance: 100000, currency: "ARS" };
|
|
|
|
|
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(expense);
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockPrisma.quoteHistory.findFirst
|
|
|
|
|
.mockResolvedValueOnce(null)
|
|
|
|
|
.mockResolvedValueOnce(null);
|
|
|
|
|
@@ -780,10 +858,13 @@ describe("payExpense with BELO", () => {
|
|
|
|
|
paymentDate: data.paymentDate,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 85000 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await payExpense(1, {
|
|
|
|
|
amountPayed: 15000,
|
|
|
|
|
paymentDate: "2026-06-10T00:00:00.000Z",
|
|
|
|
|
walletId: 1,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.beloPrice).toBeUndefined();
|
|
|
|
|
@@ -794,6 +875,8 @@ describe("payExpense with BELO", () => {
|
|
|
|
|
describe("createNonPeriodicExpense with BELO", () => {
|
|
|
|
|
test("sets beloPrice and usdcEquivalent when BELO quote is available", async () => {
|
|
|
|
|
const dueDate = "2026-06-15T00:00:00.000Z";
|
|
|
|
|
const wallet = { id: 1, balance: 100000, currency: "ARS" };
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockPrisma.quoteHistory.findFirst
|
|
|
|
|
.mockResolvedValueOnce({
|
|
|
|
|
sell: "1500.00",
|
|
|
|
|
@@ -812,11 +895,14 @@ describe("createNonPeriodicExpense with BELO", () => {
|
|
|
|
|
usdcEquivalent: data.usdcEquivalent,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 70000 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await createNonPeriodicExpense({
|
|
|
|
|
description: "Ropa",
|
|
|
|
|
amount: 30000,
|
|
|
|
|
dueDate,
|
|
|
|
|
walletId: 1,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.beloPrice).toBe(1500);
|
|
|
|
|
@@ -824,6 +910,8 @@ describe("createNonPeriodicExpense with BELO", () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("does not set BELO fields when no quote is available", async () => {
|
|
|
|
|
const wallet = { id: 1, balance: 100000, currency: "ARS" };
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockPrisma.quoteHistory.findFirst
|
|
|
|
|
.mockResolvedValueOnce(null)
|
|
|
|
|
.mockResolvedValueOnce(null);
|
|
|
|
|
@@ -837,11 +925,14 @@ describe("createNonPeriodicExpense with BELO", () => {
|
|
|
|
|
status: PaymentStatus.PAYED,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 97500 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await createNonPeriodicExpense({
|
|
|
|
|
description: "Ropa",
|
|
|
|
|
amount: 2500,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
walletId: 1,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.beloPrice).toBeUndefined();
|
|
|
|
|
@@ -938,3 +1029,402 @@ describe("updateExpense with BELO", () => {
|
|
|
|
|
expect(mockPrisma.quoteHistory.findFirst).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("updateExpense wallet adjustments", () => {
|
|
|
|
|
test("creates WITHDRAWAL when amount increases on a PAYED expense", async () => {
|
|
|
|
|
const existing = {
|
|
|
|
|
id: 1,
|
|
|
|
|
status: PaymentStatus.PAYED,
|
|
|
|
|
amount: 25000,
|
|
|
|
|
amountPayed: 25000,
|
|
|
|
|
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
|
|
|
|
|
dueDate: new Date("2026-06-15T00:00:00.000Z"),
|
|
|
|
|
year: 2026,
|
|
|
|
|
month: 6,
|
|
|
|
|
walletId: 1,
|
|
|
|
|
};
|
|
|
|
|
const wallet = { id: 1, balance: 100000, currency: "ARS" };
|
|
|
|
|
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockPrisma.expense.update.mockImplementationOnce(
|
|
|
|
|
async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
|
|
|
|
|
);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 90000 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await updateExpense(1, {
|
|
|
|
|
amount: 35000,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.amount).toBe(35000);
|
|
|
|
|
expect(mockWalletUpdate).toHaveBeenCalledWith({
|
|
|
|
|
where: { id: 1 },
|
|
|
|
|
data: { balance: { increment: -10000 } },
|
|
|
|
|
});
|
|
|
|
|
expect(mockMovementCreate).toHaveBeenCalledWith({
|
|
|
|
|
data: {
|
|
|
|
|
walletId: 1,
|
|
|
|
|
type: "WITHDRAWAL",
|
|
|
|
|
amount: -10000,
|
|
|
|
|
description: "Ajuste por edición de gasto",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("creates DEPOSIT when amount decreases on a PAYED expense", async () => {
|
|
|
|
|
const existing = {
|
|
|
|
|
id: 1,
|
|
|
|
|
status: PaymentStatus.PAYED,
|
|
|
|
|
amount: 35000,
|
|
|
|
|
amountPayed: 35000,
|
|
|
|
|
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
|
|
|
|
|
dueDate: new Date("2026-06-15T00:00:00.000Z"),
|
|
|
|
|
year: 2026,
|
|
|
|
|
month: 6,
|
|
|
|
|
walletId: 1,
|
|
|
|
|
};
|
|
|
|
|
const wallet = { id: 1, balance: 75000, currency: "ARS" };
|
|
|
|
|
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockPrisma.expense.update.mockImplementationOnce(
|
|
|
|
|
async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
|
|
|
|
|
);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 85000 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await updateExpense(1, {
|
|
|
|
|
amount: 25000,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.amount).toBe(25000);
|
|
|
|
|
expect(mockWalletUpdate).toHaveBeenCalledWith({
|
|
|
|
|
where: { id: 1 },
|
|
|
|
|
data: { balance: { increment: 10000 } },
|
|
|
|
|
});
|
|
|
|
|
expect(mockMovementCreate).toHaveBeenCalledWith({
|
|
|
|
|
data: {
|
|
|
|
|
walletId: 1,
|
|
|
|
|
type: "DEPOSIT",
|
|
|
|
|
amount: 10000,
|
|
|
|
|
description: "Ajuste por edición de gasto",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("does not create wallet adjustment when amount is unchanged", async () => {
|
|
|
|
|
const existing = {
|
|
|
|
|
id: 1,
|
|
|
|
|
status: PaymentStatus.PAYED,
|
|
|
|
|
amount: 25000,
|
|
|
|
|
amountPayed: 25000,
|
|
|
|
|
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
|
|
|
|
|
dueDate: new Date("2026-06-15T00:00:00.000Z"),
|
|
|
|
|
year: 2026,
|
|
|
|
|
month: 6,
|
|
|
|
|
walletId: 1,
|
|
|
|
|
};
|
|
|
|
|
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
|
|
|
|
|
mockPrisma.expense.update.mockImplementationOnce(
|
|
|
|
|
async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await updateExpense(1, {
|
|
|
|
|
amount: 25000,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.amount).toBe(25000);
|
|
|
|
|
expect(mockWalletUpdate).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockMovementCreate).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("does not create wallet adjustment when existing expense has no walletId", async () => {
|
|
|
|
|
const existing = {
|
|
|
|
|
id: 1,
|
|
|
|
|
status: PaymentStatus.PAYED,
|
|
|
|
|
amount: 25000,
|
|
|
|
|
amountPayed: 25000,
|
|
|
|
|
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
|
|
|
|
|
dueDate: new Date("2026-06-15T00: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: 35000,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.amount).toBe(35000);
|
|
|
|
|
expect(mockWalletUpdate).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockMovementCreate).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("creates DEPOSIT refund when reverting PAYED expense to PENDING with walletId", async () => {
|
|
|
|
|
const existing = {
|
|
|
|
|
id: 1,
|
|
|
|
|
status: PaymentStatus.PAYED,
|
|
|
|
|
amount: 25000,
|
|
|
|
|
amountPayed: 25000,
|
|
|
|
|
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
|
|
|
|
|
dueDate: new Date("2026-06-15T00:00:00.000Z"),
|
|
|
|
|
year: 2026,
|
|
|
|
|
month: 6,
|
|
|
|
|
walletId: 1,
|
|
|
|
|
};
|
|
|
|
|
const wallet = { id: 1, balance: 75000, currency: "ARS" };
|
|
|
|
|
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockPrisma.expense.update.mockImplementationOnce(
|
|
|
|
|
async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
|
|
|
|
|
);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 100000 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await updateExpense(1, {
|
|
|
|
|
amount: 25000,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
status: "PENDING",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe(PaymentStatus.PENDING);
|
|
|
|
|
expect(result.amountPayed).toBeNull();
|
|
|
|
|
expect(mockWalletUpdate).toHaveBeenCalledWith({
|
|
|
|
|
where: { id: 1 },
|
|
|
|
|
data: { balance: { increment: 25000 } },
|
|
|
|
|
});
|
|
|
|
|
expect(mockMovementCreate).toHaveBeenCalledWith({
|
|
|
|
|
data: {
|
|
|
|
|
walletId: 1,
|
|
|
|
|
type: "DEPOSIT",
|
|
|
|
|
amount: 25000,
|
|
|
|
|
description: "Anulación de gasto",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("does not create refund when existing expense has no walletId for PENDING revert", async () => {
|
|
|
|
|
const existing = {
|
|
|
|
|
id: 1,
|
|
|
|
|
status: PaymentStatus.PAYED,
|
|
|
|
|
amount: 25000,
|
|
|
|
|
amountPayed: 25000,
|
|
|
|
|
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
|
|
|
|
|
dueDate: new Date("2026-06-15T00: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: 25000,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
status: "PENDING",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe(PaymentStatus.PENDING);
|
|
|
|
|
expect(mockWalletUpdate).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockMovementCreate).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("throws when wallet is not found during adjustment", async () => {
|
|
|
|
|
const existing = {
|
|
|
|
|
id: 1,
|
|
|
|
|
status: PaymentStatus.PAYED,
|
|
|
|
|
amount: 25000,
|
|
|
|
|
amountPayed: 25000,
|
|
|
|
|
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
|
|
|
|
|
dueDate: new Date("2026-06-15T00:00:00.000Z"),
|
|
|
|
|
year: 2026,
|
|
|
|
|
month: 6,
|
|
|
|
|
walletId: 1,
|
|
|
|
|
};
|
|
|
|
|
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(null);
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
updateExpense(1, {
|
|
|
|
|
amount: 35000,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
}),
|
|
|
|
|
).rejects.toThrow("Billetera no encontrada");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("only creates refund (not amount adjustment) when both amount and status change", async () => {
|
|
|
|
|
const existing = {
|
|
|
|
|
id: 1,
|
|
|
|
|
status: PaymentStatus.PAYED,
|
|
|
|
|
amount: 25000,
|
|
|
|
|
amountPayed: 25000,
|
|
|
|
|
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
|
|
|
|
|
dueDate: new Date("2026-06-15T00:00:00.000Z"),
|
|
|
|
|
year: 2026,
|
|
|
|
|
month: 6,
|
|
|
|
|
walletId: 1,
|
|
|
|
|
};
|
|
|
|
|
const wallet = { id: 1, balance: 75000, currency: "ARS" };
|
|
|
|
|
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockPrisma.expense.update.mockImplementationOnce(
|
|
|
|
|
async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
|
|
|
|
|
);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 100000 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await updateExpense(1, {
|
|
|
|
|
amount: 35000,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
status: "PENDING",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe(PaymentStatus.PENDING);
|
|
|
|
|
expect(mockWalletUpdate).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(mockWalletUpdate).toHaveBeenCalledWith({
|
|
|
|
|
where: { id: 1 },
|
|
|
|
|
data: { balance: { increment: 25000 } },
|
|
|
|
|
});
|
|
|
|
|
expect(mockMovementCreate).toHaveBeenCalledWith({
|
|
|
|
|
data: {
|
|
|
|
|
walletId: 1,
|
|
|
|
|
type: "DEPOSIT",
|
|
|
|
|
amount: 25000,
|
|
|
|
|
description: "Anulación de gasto",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("uses amountPayed for refund when expense has different amountPayed and amount", async () => {
|
|
|
|
|
const existing = {
|
|
|
|
|
id: 1,
|
|
|
|
|
status: PaymentStatus.PAYED,
|
|
|
|
|
amount: 30000,
|
|
|
|
|
amountPayed: 28000,
|
|
|
|
|
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
|
|
|
|
|
dueDate: new Date("2026-06-15T00:00:00.000Z"),
|
|
|
|
|
year: 2026,
|
|
|
|
|
month: 6,
|
|
|
|
|
walletId: 1,
|
|
|
|
|
};
|
|
|
|
|
const wallet = { id: 1, balance: 75000, currency: "ARS" };
|
|
|
|
|
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockPrisma.expense.update.mockImplementationOnce(
|
|
|
|
|
async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
|
|
|
|
|
);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 103000 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await updateExpense(1, {
|
|
|
|
|
amount: 30000,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
status: "PENDING",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe(PaymentStatus.PENDING);
|
|
|
|
|
expect(mockWalletUpdate).toHaveBeenCalledWith({
|
|
|
|
|
where: { id: 1 },
|
|
|
|
|
data: { balance: { increment: 28000 } },
|
|
|
|
|
});
|
|
|
|
|
expect(mockMovementCreate).toHaveBeenCalledWith({
|
|
|
|
|
data: {
|
|
|
|
|
walletId: 1,
|
|
|
|
|
type: "DEPOSIT",
|
|
|
|
|
amount: 28000,
|
|
|
|
|
description: "Anulación de gasto",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("creates WITHDRAWAL when amountPayed increases while amount stays the same", async () => {
|
|
|
|
|
const existing = {
|
|
|
|
|
id: 1,
|
|
|
|
|
status: PaymentStatus.PAYED,
|
|
|
|
|
amount: 25000,
|
|
|
|
|
amountPayed: 25000,
|
|
|
|
|
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
|
|
|
|
|
dueDate: new Date("2026-06-15T00:00:00.000Z"),
|
|
|
|
|
year: 2026,
|
|
|
|
|
month: 6,
|
|
|
|
|
walletId: 1,
|
|
|
|
|
};
|
|
|
|
|
const wallet = { id: 1, balance: 75000, currency: "ARS" };
|
|
|
|
|
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockPrisma.expense.update.mockImplementationOnce(
|
|
|
|
|
async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
|
|
|
|
|
);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 65000 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await updateExpense(1, {
|
|
|
|
|
amount: 25000,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
amountPayed: 35000,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.amount).toBe(25000);
|
|
|
|
|
expect(mockWalletUpdate).toHaveBeenCalledWith({
|
|
|
|
|
where: { id: 1 },
|
|
|
|
|
data: { balance: { increment: -10000 } },
|
|
|
|
|
});
|
|
|
|
|
expect(mockMovementCreate).toHaveBeenCalledWith({
|
|
|
|
|
data: {
|
|
|
|
|
walletId: 1,
|
|
|
|
|
type: "WITHDRAWAL",
|
|
|
|
|
amount: -10000,
|
|
|
|
|
description: "Ajuste por edición de gasto",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("creates adjustment based on amountPayed when both amount and amountPayed change", async () => {
|
|
|
|
|
const existing = {
|
|
|
|
|
id: 1,
|
|
|
|
|
status: PaymentStatus.PAYED,
|
|
|
|
|
amount: 25000,
|
|
|
|
|
amountPayed: 25000,
|
|
|
|
|
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
|
|
|
|
|
dueDate: new Date("2026-06-15T00:00:00.000Z"),
|
|
|
|
|
year: 2026,
|
|
|
|
|
month: 6,
|
|
|
|
|
walletId: 1,
|
|
|
|
|
};
|
|
|
|
|
const wallet = { id: 1, balance: 75000, currency: "ARS" };
|
|
|
|
|
mockPrisma.expense.findUniqueOrThrow.mockResolvedValueOnce(existing);
|
|
|
|
|
mockWalletFindUnique.mockResolvedValueOnce(wallet);
|
|
|
|
|
mockPrisma.expense.update.mockImplementationOnce(
|
|
|
|
|
async ({ where, data }) => ({ id: where.id, ...existing, ...data }),
|
|
|
|
|
);
|
|
|
|
|
mockWalletUpdate.mockResolvedValueOnce({ ...wallet, balance: 72000 });
|
|
|
|
|
mockMovementCreate.mockResolvedValueOnce({ id: 1 });
|
|
|
|
|
|
|
|
|
|
const result = await updateExpense(1, {
|
|
|
|
|
amount: 30000,
|
|
|
|
|
dueDate: "2026-06-15T00:00:00.000Z",
|
|
|
|
|
amountPayed: 28000,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.amount).toBe(30000);
|
|
|
|
|
expect(mockWalletUpdate).toHaveBeenCalledWith({
|
|
|
|
|
where: { id: 1 },
|
|
|
|
|
data: { balance: { increment: -3000 } },
|
|
|
|
|
});
|
|
|
|
|
expect(mockMovementCreate).toHaveBeenCalledWith({
|
|
|
|
|
data: {
|
|
|
|
|
walletId: 1,
|
|
|
|
|
type: "WITHDRAWAL",
|
|
|
|
|
amount: -3000,
|
|
|
|
|
description: "Ajuste por edición de gasto",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|