feat(expenses): add beloPrice and usdcEquivalent fields to Expense model and update related logic

This commit is contained in:
Jose Selesan
2026-06-08 21:13:03 -03:00
parent b08399908b
commit 142fd4e33f
6 changed files with 398 additions and 2 deletions

View File

@@ -16,6 +16,12 @@ const mockPrisma = {
update: mock(),
count: mock(),
},
quote: {
findFirst: mock(),
},
quoteHistory: {
findFirst: mock(),
},
};
mock.module("../src/lib/prisma", () => ({
@@ -40,6 +46,7 @@ const {
updateExpenseSchema,
buildDueDate,
getCurrentYearMonthUTC,
getBeloSellPriceForDate,
listPeriodicExpenses,
createPeriodicExpense,
updatePeriodicExpense,
@@ -663,3 +670,271 @@ describe("generateMonthlyExpense", () => {
expect(result).toEqual(existing);
});
});
describe("getBeloSellPriceForDate", () => {
test("returns current BELO sell price when paymentDate is today", async () => {
const today = new Date();
mockPrisma.quote.findFirst.mockResolvedValueOnce({
sell: "1500.00",
});
const result = await getBeloSellPriceForDate(today);
expect(result).toBe(1500);
expect(mockPrisma.quote.findFirst).toHaveBeenCalledWith({
where: { type: "BELO" },
});
});
test("returns null when no current BELO quote exists for today", async () => {
mockPrisma.quote.findFirst.mockResolvedValueOnce(null);
const result = await getBeloSellPriceForDate(new Date());
expect(result).toBeNull();
});
test("returns historical BELO sell price closest to 17:00 for a past date", async () => {
const pastDate = new Date("2026-06-05T10:00:00.000Z");
const before = {
sell: "1400.50",
timeStamp: new Date("2026-06-05T16:50:00.000Z"),
};
const after = {
sell: "1410.00",
timeStamp: new Date("2026-06-05T17:05:00.000Z"),
};
mockPrisma.quoteHistory.findFirst
.mockResolvedValueOnce(before)
.mockResolvedValueOnce(after);
const result = await getBeloSellPriceForDate(pastDate);
expect(result).toBe(1410);
expect(mockPrisma.quoteHistory.findFirst).toHaveBeenCalledTimes(2);
});
test("returns the only available historical entry if only one exists", async () => {
const pastDate = new Date("2026-06-05T10:00:00.000Z");
const before = {
sell: "1400.50",
timeStamp: new Date("2026-06-05T16:50:00.000Z"),
};
mockPrisma.quoteHistory.findFirst
.mockResolvedValueOnce(before)
.mockResolvedValueOnce(null);
const result = await getBeloSellPriceForDate(pastDate);
expect(result).toBe(1400.5);
});
test("returns null when no historical data exists for a past date", async () => {
mockPrisma.quoteHistory.findFirst
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(null);
const result = await getBeloSellPriceForDate(
new Date("2026-06-05T10:00:00.000Z"),
);
expect(result).toBeNull();
});
});
describe("payExpense with BELO", () => {
test("sets beloPrice and usdcEquivalent when BELO quote is available", async () => {
const paymentDate = new Date("2026-06-10T00:00:00.000Z");
mockPrisma.quoteHistory.findFirst
.mockResolvedValueOnce({
sell: "1500.00",
timeStamp: new Date("2026-06-10T17:00:00.000Z"),
})
.mockResolvedValueOnce(null);
mockPrisma.expense.update.mockImplementationOnce(
async ({ where, data }) => ({
id: where.id,
status: PaymentStatus.PAYED,
amountPayed: data.amountPayed,
paymentDate: data.paymentDate,
beloPrice: data.beloPrice,
usdcEquivalent: data.usdcEquivalent,
}),
);
const result = await payExpense(1, {
amountPayed: 15000,
paymentDate: "2026-06-10T00:00:00.000Z",
});
expect(result.beloPrice).toBe(1500);
expect(result.usdcEquivalent).toBe(10);
});
test("does not set BELO fields when no quote is available", async () => {
mockPrisma.quoteHistory.findFirst
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(null);
mockPrisma.expense.update.mockImplementationOnce(
async ({ where, data }) => ({
id: where.id,
status: PaymentStatus.PAYED,
amountPayed: data.amountPayed,
paymentDate: data.paymentDate,
}),
);
const result = await payExpense(1, {
amountPayed: 15000,
paymentDate: "2026-06-10T00:00:00.000Z",
});
expect(result.beloPrice).toBeUndefined();
expect(result.usdcEquivalent).toBeUndefined();
});
});
describe("createNonPeriodicExpense with BELO", () => {
test("sets beloPrice and usdcEquivalent when BELO quote is available", async () => {
const dueDate = "2026-06-15T00:00:00.000Z";
mockPrisma.quoteHistory.findFirst
.mockResolvedValueOnce({
sell: "1500.00",
timeStamp: new Date("2026-06-15T17:00:00.000Z"),
})
.mockResolvedValueOnce(null);
mockPrisma.expense.create.mockImplementationOnce(
async ({ data }) => ({
id: 1,
description: data.description,
amount: data.amount,
amountPayed: data.amountPayed,
status: PaymentStatus.PAYED,
beloPrice: data.beloPrice,
usdcEquivalent: data.usdcEquivalent,
}),
);
const result = await createNonPeriodicExpense({
description: "Ropa",
amount: 30000,
dueDate,
});
expect(result.beloPrice).toBe(1500);
expect(result.usdcEquivalent).toBe(20);
});
test("does not set BELO fields when no quote is available", async () => {
mockPrisma.quoteHistory.findFirst
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(null);
mockPrisma.expense.create.mockImplementationOnce(
async ({ data }) => ({
id: 1,
description: data.description,
amount: data.amount,
amountPayed: data.amountPayed,
status: PaymentStatus.PAYED,
}),
);
const result = await createNonPeriodicExpense({
description: "Ropa",
amount: 2500,
dueDate: "2026-06-15T00:00:00.000Z",
});
expect(result.beloPrice).toBeUndefined();
expect(result.usdcEquivalent).toBeUndefined();
});
});
describe("updateExpense with BELO", () => {
test("recalculates BELO when amountPayed changes on a PAYED expense", async () => {
const existing = {
id: 1,
status: PaymentStatus.PAYED,
amount: 1500,
amountPayed: 1500,
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.quoteHistory.findFirst
.mockResolvedValueOnce({
sell: "1600.00",
timeStamp: new Date("2026-06-10T17:00:00.000Z"),
})
.mockResolvedValueOnce(null);
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",
amountPayed: 1600,
});
expect(result.beloPrice).toBe(1600);
expect(result.usdcEquivalent).toBe(1);
});
test("clears BELO fields when reverting a PAYED expense to PENDING", async () => {
const existing = {
id: 1,
status: PaymentStatus.PAYED,
amount: 1500,
amountPayed: 1500,
paymentDate: new Date("2026-06-10T00:00:00.000Z"),
dueDate: new Date("2026-06-15T00:00:00.000Z"),
year: 2026,
month: 6,
beloPrice: 1500,
usdcEquivalent: 1,
};
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.beloPrice).toBeNull();
expect(result.usdcEquivalent).toBeNull();
});
test("does not query BELO when updating a PENDING expense", async () => {
const existing = {
id: 1,
status: PaymentStatus.PENDING,
amount: 1500,
amountPayed: null,
paymentDate: null,
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: 2000,
dueDate: "2026-07-20T00:00:00.000Z",
});
expect(result.beloPrice).toBeUndefined();
expect(mockPrisma.quote.findFirst).not.toHaveBeenCalled();
expect(mockPrisma.quoteHistory.findFirst).not.toHaveBeenCalled();
});
});