feat(expenses): add beloPrice and usdcEquivalent fields to Expense model and update related logic
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user