feat/payment-wallets #5

Merged
jselesan merged 3 commits from feat/payment-wallets into main 2026-06-17 14:11:31 +00:00
13 changed files with 1027 additions and 45 deletions
Showing only changes of commit 33120deed6 - Show all commits

View File

@@ -0,0 +1,8 @@
-- AlterTable
ALTER TABLE "expenses" ADD COLUMN "walletId" INTEGER;
-- CreateIndex
CREATE INDEX "expenses_walletId_idx" ON "expenses"("walletId");
-- AddForeignKey
ALTER TABLE "expenses" ADD CONSTRAINT "expenses_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "wallets"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -139,6 +139,7 @@ model Wallet {
updatedAt DateTime @updatedAt
movements WalletMovement[]
expenses Expense[]
@@map("wallets")
}
@@ -187,6 +188,9 @@ model Expense {
status PaymentStatus @default(PENDING)
dueDate DateTime
paymentDate DateTime?
walletId Int?
wallet Wallet? @relation(fields: [walletId], references: [id])
@@index([walletId])
@@map("expenses")
}

View File

@@ -259,6 +259,7 @@ export async function createNonPeriodicExpense(
year,
month,
status: PaymentStatus.PAYED,
walletId: data.walletId,
};
if (isUSDC && data.usdcConversionRate) {
@@ -325,6 +326,7 @@ export async function payExpense(
status: PaymentStatus.PAYED,
amountPayed: data.amountPayed,
paymentDate,
walletId: data.walletId,
};
if (isUSDC && data.usdcConversionRate) {
@@ -372,12 +374,29 @@ export async function updateExpense(
month,
};
let walletAdjustment: {
walletId: number;
amount: number;
description: string;
} | null = null;
if (data.status === "PENDING") {
updateData.status = PaymentStatus.PENDING;
updateData.amountPayed = null;
updateData.paymentDate = null;
updateData.beloPrice = null;
updateData.usdcEquivalent = null;
if (existing.status === PaymentStatus.PAYED && existing.walletId) {
const refundAmount = existing.amountPayed
? Number(existing.amountPayed)
: Number(existing.amount);
walletAdjustment = {
walletId: existing.walletId,
amount: refundAmount,
description: "Anulación de gasto",
};
}
} else if (existing.status === PaymentStatus.PAYED) {
if (data.amountPayed !== undefined) {
updateData.amountPayed = data.amountPayed;
@@ -400,6 +419,51 @@ export async function updateExpense(
updateData.usdcEquivalent = null;
}
}
if (existing.walletId) {
const oldPayed = existing.amountPayed
? Number(existing.amountPayed)
: Number(existing.amount);
const newPayed = data.amountPayed !== undefined
? Number(data.amountPayed)
: Number(data.amount);
if (newPayed !== oldPayed) {
const difference = newPayed - oldPayed;
walletAdjustment = {
walletId: existing.walletId,
amount: -difference,
description: "Ajuste por edición de gasto",
};
}
}
}
if (walletAdjustment) {
const wallet = await prisma.wallet.findUnique({
where: { id: walletAdjustment.walletId },
});
if (!wallet) {
throw new Error("Billetera no encontrada");
}
const [result] = await prisma.$transaction([
prisma.expense.update({ where: { id }, data: updateData }),
prisma.wallet.update({
where: { id: walletAdjustment.walletId },
data: { balance: { increment: walletAdjustment.amount } },
}),
prisma.walletMovement.create({
data: {
walletId: walletAdjustment.walletId,
type: walletAdjustment.amount > 0 ? "DEPOSIT" : "WITHDRAWAL",
amount: walletAdjustment.amount,
description: walletAdjustment.description,
},
}),
]);
cache.invalidateByPrefix("expenses:");
return result;
}
const result = await prisma.expense.update({

View File

@@ -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",
},
});
});
});