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