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

@@ -1,7 +1,8 @@
import { z } from "zod";
import { PaymentStatus } from "../../generated/prisma/client";
import { PaymentStatus, QuoteType, Prisma } from "../../generated/prisma/client";
import { cache } from "../../lib/cache";
import { prisma } from "../../lib/prisma";
import { roundTo } from "../../lib/utils";
export const createPeriodicExpenseSchema = z.object({
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 };
}
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() {
const cached = cache.get("expenses:periodic");
if (cached) return cached;
@@ -168,6 +225,9 @@ export async function createNonPeriodicExpense(
const dueDate = new Date(data.dueDate);
const year = dueDate.getUTCFullYear();
const month = dueDate.getUTCMonth() + 1;
const belo = await getBeloSellPriceForDate(dueDate);
const result = await prisma.expense.create({
data: {
description: data.description,
@@ -178,6 +238,9 @@ export async function createNonPeriodicExpense(
year,
month,
status: PaymentStatus.PAYED,
...(belo !== null
? { beloPrice: belo, usdcEquivalent: roundTo(data.amount / belo, 6) }
: {}),
},
});
cache.invalidateByPrefix("expenses:");
@@ -191,12 +254,18 @@ export async function payExpense(
const paymentDate = data.paymentDate
? new Date(data.paymentDate)
: new Date();
const belo = await getBeloSellPriceForDate(paymentDate);
const result = await prisma.expense.update({
where: { id },
data: {
status: PaymentStatus.PAYED,
amountPayed: data.amountPayed,
paymentDate,
...(belo !== null
? { beloPrice: belo, usdcEquivalent: roundTo(data.amountPayed / belo, 6) }
: {}),
},
});
cache.invalidateByPrefix("expenses:");
@@ -213,7 +282,7 @@ export async function updateExpense(
const year = dueDate.getUTCFullYear();
const month = dueDate.getUTCMonth() + 1;
const updateData: Record<string, unknown> = {
const updateData: Prisma.ExpenseUncheckedUpdateInput = {
amount: data.amount,
dueDate,
year,
@@ -224,6 +293,8 @@ export async function updateExpense(
updateData.status = PaymentStatus.PENDING;
updateData.amountPayed = null;
updateData.paymentDate = null;
updateData.beloPrice = null;
updateData.usdcEquivalent = null;
} else if (existing.status === PaymentStatus.PAYED) {
if (data.amountPayed !== undefined) {
updateData.amountPayed = data.amountPayed;
@@ -231,6 +302,21 @@ export async function updateExpense(
if (data.paymentDate !== undefined) {
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({