feat(expenses): add beloPrice and usdcEquivalent fields to Expense model and update related logic
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "expenses" ADD COLUMN "beloPrice" MONEY,
|
||||||
|
ADD COLUMN "usdcEquivalent" DECIMAL(12,6);
|
||||||
@@ -130,6 +130,8 @@ model Expense {
|
|||||||
month Int
|
month Int
|
||||||
amount Decimal @db.Money
|
amount Decimal @db.Money
|
||||||
amountPayed Decimal? @db.Money
|
amountPayed Decimal? @db.Money
|
||||||
|
beloPrice Decimal? @db.Money
|
||||||
|
usdcEquivalent Decimal? @db.Decimal(12, 6)
|
||||||
status PaymentStatus @default(PENDING)
|
status PaymentStatus @default(PENDING)
|
||||||
dueDate DateTime
|
dueDate DateTime
|
||||||
paymentDate DateTime?
|
paymentDate DateTime?
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { PaymentStatus } from "../../generated/prisma/client";
|
import { PaymentStatus, QuoteType, Prisma } from "../../generated/prisma/client";
|
||||||
import { cache } from "../../lib/cache";
|
import { cache } from "../../lib/cache";
|
||||||
import { prisma } from "../../lib/prisma";
|
import { prisma } from "../../lib/prisma";
|
||||||
|
import { roundTo } from "../../lib/utils";
|
||||||
|
|
||||||
export const createPeriodicExpenseSchema = z.object({
|
export const createPeriodicExpenseSchema = z.object({
|
||||||
description: z.string().min(1).max(50),
|
description: z.string().min(1).max(50),
|
||||||
@@ -57,6 +58,62 @@ export function getCurrentYearMonthUTC(): { year: number; month: number } {
|
|||||||
return { year: now.getUTCFullYear(), month: now.getUTCMonth() + 1 };
|
return { year: now.getUTCFullYear(), month: now.getUTCMonth() + 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSameDay(a: Date, b: Date): boolean {
|
||||||
|
return (
|
||||||
|
a.getUTCFullYear() === b.getUTCFullYear() &&
|
||||||
|
a.getUTCMonth() === b.getUTCMonth() &&
|
||||||
|
a.getUTCDate() === b.getUTCDate()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBeloSellPriceForDate(
|
||||||
|
paymentDate: Date,
|
||||||
|
): Promise<number | null> {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
if (isSameDay(paymentDate, now)) {
|
||||||
|
const quote = await prisma.quote.findFirst({
|
||||||
|
where: { type: QuoteType.BELO },
|
||||||
|
});
|
||||||
|
return quote ? Number(quote.sell) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = new Date(
|
||||||
|
Date.UTC(
|
||||||
|
paymentDate.getUTCFullYear(),
|
||||||
|
paymentDate.getUTCMonth(),
|
||||||
|
paymentDate.getUTCDate(),
|
||||||
|
17, 0, 0, 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const before = await prisma.quoteHistory.findFirst({
|
||||||
|
where: {
|
||||||
|
type: QuoteType.BELO,
|
||||||
|
timeStamp: { lte: target },
|
||||||
|
},
|
||||||
|
orderBy: { timeStamp: "desc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const after = await prisma.quoteHistory.findFirst({
|
||||||
|
where: {
|
||||||
|
type: QuoteType.BELO,
|
||||||
|
timeStamp: { gte: target },
|
||||||
|
},
|
||||||
|
orderBy: { timeStamp: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (before && after) {
|
||||||
|
const beforeDiff = Math.abs(before.timeStamp.getTime() - target.getTime());
|
||||||
|
const afterDiff = Math.abs(after.timeStamp.getTime() - target.getTime());
|
||||||
|
return beforeDiff <= afterDiff ? Number(before.sell) : Number(after.sell);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (before) return Number(before.sell);
|
||||||
|
if (after) return Number(after.sell);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function listPeriodicExpenses() {
|
export async function listPeriodicExpenses() {
|
||||||
const cached = cache.get("expenses:periodic");
|
const cached = cache.get("expenses:periodic");
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
@@ -168,6 +225,9 @@ export async function createNonPeriodicExpense(
|
|||||||
const dueDate = new Date(data.dueDate);
|
const dueDate = new Date(data.dueDate);
|
||||||
const year = dueDate.getUTCFullYear();
|
const year = dueDate.getUTCFullYear();
|
||||||
const month = dueDate.getUTCMonth() + 1;
|
const month = dueDate.getUTCMonth() + 1;
|
||||||
|
|
||||||
|
const belo = await getBeloSellPriceForDate(dueDate);
|
||||||
|
|
||||||
const result = await prisma.expense.create({
|
const result = await prisma.expense.create({
|
||||||
data: {
|
data: {
|
||||||
description: data.description,
|
description: data.description,
|
||||||
@@ -178,6 +238,9 @@ export async function createNonPeriodicExpense(
|
|||||||
year,
|
year,
|
||||||
month,
|
month,
|
||||||
status: PaymentStatus.PAYED,
|
status: PaymentStatus.PAYED,
|
||||||
|
...(belo !== null
|
||||||
|
? { beloPrice: belo, usdcEquivalent: roundTo(data.amount / belo, 6) }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
cache.invalidateByPrefix("expenses:");
|
cache.invalidateByPrefix("expenses:");
|
||||||
@@ -191,12 +254,18 @@ export async function payExpense(
|
|||||||
const paymentDate = data.paymentDate
|
const paymentDate = data.paymentDate
|
||||||
? new Date(data.paymentDate)
|
? new Date(data.paymentDate)
|
||||||
: new Date();
|
: new Date();
|
||||||
|
|
||||||
|
const belo = await getBeloSellPriceForDate(paymentDate);
|
||||||
|
|
||||||
const result = await prisma.expense.update({
|
const result = await prisma.expense.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
status: PaymentStatus.PAYED,
|
status: PaymentStatus.PAYED,
|
||||||
amountPayed: data.amountPayed,
|
amountPayed: data.amountPayed,
|
||||||
paymentDate,
|
paymentDate,
|
||||||
|
...(belo !== null
|
||||||
|
? { beloPrice: belo, usdcEquivalent: roundTo(data.amountPayed / belo, 6) }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
cache.invalidateByPrefix("expenses:");
|
cache.invalidateByPrefix("expenses:");
|
||||||
@@ -213,7 +282,7 @@ export async function updateExpense(
|
|||||||
const year = dueDate.getUTCFullYear();
|
const year = dueDate.getUTCFullYear();
|
||||||
const month = dueDate.getUTCMonth() + 1;
|
const month = dueDate.getUTCMonth() + 1;
|
||||||
|
|
||||||
const updateData: Record<string, unknown> = {
|
const updateData: Prisma.ExpenseUncheckedUpdateInput = {
|
||||||
amount: data.amount,
|
amount: data.amount,
|
||||||
dueDate,
|
dueDate,
|
||||||
year,
|
year,
|
||||||
@@ -224,6 +293,8 @@ export async function updateExpense(
|
|||||||
updateData.status = PaymentStatus.PENDING;
|
updateData.status = PaymentStatus.PENDING;
|
||||||
updateData.amountPayed = null;
|
updateData.amountPayed = null;
|
||||||
updateData.paymentDate = null;
|
updateData.paymentDate = null;
|
||||||
|
updateData.beloPrice = null;
|
||||||
|
updateData.usdcEquivalent = null;
|
||||||
} else if (existing.status === PaymentStatus.PAYED) {
|
} else if (existing.status === PaymentStatus.PAYED) {
|
||||||
if (data.amountPayed !== undefined) {
|
if (data.amountPayed !== undefined) {
|
||||||
updateData.amountPayed = data.amountPayed;
|
updateData.amountPayed = data.amountPayed;
|
||||||
@@ -231,6 +302,21 @@ export async function updateExpense(
|
|||||||
if (data.paymentDate !== undefined) {
|
if (data.paymentDate !== undefined) {
|
||||||
updateData.paymentDate = new Date(data.paymentDate);
|
updateData.paymentDate = new Date(data.paymentDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedPaymentDate = updateData.paymentDate instanceof Date
|
||||||
|
? updateData.paymentDate
|
||||||
|
: (existing.paymentDate ?? undefined);
|
||||||
|
const amountPayed = updateData.amountPayed ?? existing.amountPayed;
|
||||||
|
if (resolvedPaymentDate && amountPayed !== undefined && amountPayed !== null) {
|
||||||
|
const belo = await getBeloSellPriceForDate(resolvedPaymentDate);
|
||||||
|
if (belo !== null) {
|
||||||
|
updateData.beloPrice = belo;
|
||||||
|
updateData.usdcEquivalent = roundTo(Number(amountPayed) / belo, 6);
|
||||||
|
} else {
|
||||||
|
updateData.beloPrice = null;
|
||||||
|
updateData.usdcEquivalent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await prisma.expense.update({
|
const result = await prisma.expense.update({
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ const mockPrisma = {
|
|||||||
update: mock(),
|
update: mock(),
|
||||||
count: mock(),
|
count: mock(),
|
||||||
},
|
},
|
||||||
|
quote: {
|
||||||
|
findFirst: mock(),
|
||||||
|
},
|
||||||
|
quoteHistory: {
|
||||||
|
findFirst: mock(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
mock.module("../src/lib/prisma", () => ({
|
mock.module("../src/lib/prisma", () => ({
|
||||||
@@ -40,6 +46,7 @@ const {
|
|||||||
updateExpenseSchema,
|
updateExpenseSchema,
|
||||||
buildDueDate,
|
buildDueDate,
|
||||||
getCurrentYearMonthUTC,
|
getCurrentYearMonthUTC,
|
||||||
|
getBeloSellPriceForDate,
|
||||||
listPeriodicExpenses,
|
listPeriodicExpenses,
|
||||||
createPeriodicExpense,
|
createPeriodicExpense,
|
||||||
updatePeriodicExpense,
|
updatePeriodicExpense,
|
||||||
@@ -663,3 +670,271 @@ describe("generateMonthlyExpense", () => {
|
|||||||
expect(result).toEqual(existing);
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -97,6 +97,25 @@ export function ExpensesTable({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: "USDC",
|
||||||
|
accessorKey: "usdcEquivalent",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const value = row.getValue("usdcEquivalent");
|
||||||
|
if (!value) {
|
||||||
|
return <span className="text-muted-foreground">--</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className="tabular-nums">
|
||||||
|
{Number(value).toLocaleString("es-AR", {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})}{" "}
|
||||||
|
USDC
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: "Vencimiento",
|
header: "Vencimiento",
|
||||||
accessorKey: "dueDate",
|
accessorKey: "dueDate",
|
||||||
@@ -232,6 +251,15 @@ export function ExpensesTable({
|
|||||||
? `Pagado ${formatExpenseDate(expense.paymentDate)}`
|
? `Pagado ${formatExpenseDate(expense.paymentDate)}`
|
||||||
: "Sin fecha de pago"}
|
: "Sin fecha de pago"}
|
||||||
</p>
|
</p>
|
||||||
|
{expense.usdcEquivalent && (
|
||||||
|
<p className="text-xs text-muted-foreground tabular-nums">
|
||||||
|
{Number(expense.usdcEquivalent).toLocaleString("es-AR", {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})}{" "}
|
||||||
|
USDC
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ export type Expense = {
|
|||||||
month: number;
|
month: number;
|
||||||
amount: string;
|
amount: string;
|
||||||
amountPayed: string | null;
|
amountPayed: string | null;
|
||||||
|
beloPrice: string | null;
|
||||||
|
usdcEquivalent: string | null;
|
||||||
status: "PENDING" | "PAYED";
|
status: "PENDING" | "PAYED";
|
||||||
dueDate: string;
|
dueDate: string;
|
||||||
paymentDate: string | null;
|
paymentDate: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user