213 lines
6.3 KiB
TypeScript
213 lines
6.3 KiB
TypeScript
import { z } from "zod";
|
|
import { prisma } from "../../lib/prisma";
|
|
import { PaymentStatus } from "../../generated/prisma/client";
|
|
|
|
export const createPeriodicExpenseSchema = z.object({
|
|
description: z.string().min(1).max(50),
|
|
defaultDueDay: z.number().int().min(1).max(31),
|
|
defaultAmount: z.number().positive(),
|
|
periods: z.array(z.number().int().min(1).max(12)).min(1),
|
|
});
|
|
|
|
export const updatePeriodicExpenseSchema = createPeriodicExpenseSchema;
|
|
|
|
export const createNonPeriodicExpenseSchema = z.object({
|
|
description: z.string().min(1).max(50),
|
|
amount: z.number().positive(),
|
|
dueDate: z.string().datetime(),
|
|
});
|
|
|
|
export const payExpenseSchema = z.object({
|
|
amountPayed: z.number().positive(),
|
|
paymentDate: z.string().datetime().optional(),
|
|
});
|
|
|
|
function lastDayOfMonth(year: number, month: number): number {
|
|
return new Date(year, month, 0).getDate();
|
|
}
|
|
|
|
export function buildDueDate(year: number, month: number, dueDay: number): Date {
|
|
const clampedDay = Math.min(dueDay, lastDayOfMonth(year, month));
|
|
return new Date(Date.UTC(year, month - 1, clampedDay, 0, 0, 0, 0));
|
|
}
|
|
|
|
export function getCurrentYearMonthUTC(): { year: number; month: number } {
|
|
const now = new Date();
|
|
return { year: now.getUTCFullYear(), month: now.getUTCMonth() + 1 };
|
|
}
|
|
|
|
export async function listPeriodicExpenses() {
|
|
return prisma.periodicExpense.findMany({
|
|
where: { isDeleted: false },
|
|
orderBy: { description: "asc" },
|
|
});
|
|
}
|
|
|
|
export async function createPeriodicExpense(data: z.infer<typeof createPeriodicExpenseSchema>) {
|
|
const result = await prisma.periodicExpense.create({ data });
|
|
const { year, month } = getCurrentYearMonthUTC();
|
|
const currentMonthApplicable = data.periods.includes(month);
|
|
return { ...result, currentMonthApplicable };
|
|
}
|
|
|
|
export async function updatePeriodicExpense(id: number, data: z.infer<typeof updatePeriodicExpenseSchema>) {
|
|
return prisma.periodicExpense.update({ where: { id }, data });
|
|
}
|
|
|
|
export async function softDeletePeriodicExpense(id: number) {
|
|
return prisma.periodicExpense.update({
|
|
where: { id },
|
|
data: { isDeleted: true },
|
|
});
|
|
}
|
|
|
|
export async function generateMonthlyExpense(id: number) {
|
|
const periodic = await prisma.periodicExpense.findUniqueOrThrow({ where: { id } });
|
|
const { year, month } = getCurrentYearMonthUTC();
|
|
|
|
const existing = await prisma.expense.findFirst({
|
|
where: { periodicExpenseId: id, year, month },
|
|
});
|
|
if (existing) return existing;
|
|
|
|
const dueDate = buildDueDate(year, month, periodic.defaultDueDay);
|
|
return prisma.expense.create({
|
|
data: {
|
|
description: periodic.description,
|
|
periodicExpenseId: id,
|
|
year,
|
|
month,
|
|
amount: periodic.defaultAmount,
|
|
dueDate,
|
|
status: PaymentStatus.PENDING,
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function listExpenses(params: {
|
|
status?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
periodicExpenseId?: number;
|
|
search?: string;
|
|
}) {
|
|
const { status, page = 1, pageSize = 10, periodicExpenseId, search } = params;
|
|
const where: Record<string, unknown> = {};
|
|
if (status === "PENDING" || status === "PAYED") {
|
|
where.status = status;
|
|
}
|
|
if (periodicExpenseId !== undefined) {
|
|
where.periodicExpenseId = periodicExpenseId;
|
|
}
|
|
if (search) {
|
|
where.description = { contains: search, mode: "insensitive" };
|
|
}
|
|
const [data, total] = await Promise.all([
|
|
prisma.expense.findMany({
|
|
where,
|
|
include: { periodicExpense: true },
|
|
orderBy: { dueDate: "desc" },
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
}),
|
|
prisma.expense.count({ where }),
|
|
]);
|
|
return { data, total, page, pageSize };
|
|
}
|
|
|
|
export async function createNonPeriodicExpense(data: z.infer<typeof createNonPeriodicExpenseSchema>) {
|
|
const dueDate = new Date(data.dueDate);
|
|
const year = dueDate.getUTCFullYear();
|
|
const month = dueDate.getUTCMonth() + 1;
|
|
return prisma.expense.create({
|
|
data: {
|
|
description: data.description,
|
|
amount: data.amount,
|
|
amountPayed: data.amount,
|
|
dueDate,
|
|
paymentDate: dueDate,
|
|
year,
|
|
month,
|
|
status: PaymentStatus.PAYED,
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function payExpense(id: number, data: z.infer<typeof payExpenseSchema>) {
|
|
const paymentDate = data.paymentDate ? new Date(data.paymentDate) : new Date();
|
|
return prisma.expense.update({
|
|
where: { id },
|
|
data: {
|
|
status: PaymentStatus.PAYED,
|
|
amountPayed: data.amountPayed,
|
|
paymentDate,
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function getMonthlyPayedTotal(year: number, month: number) {
|
|
const result = await prisma.expense.aggregate({
|
|
where: { year, month, status: PaymentStatus.PAYED },
|
|
_sum: { amountPayed: true },
|
|
});
|
|
return { total: Number(result._sum.amountPayed ?? 0) };
|
|
}
|
|
|
|
export async function getMonthlyTotals(year: number, month: number) {
|
|
const [payed, pending] = await Promise.all([
|
|
prisma.expense.aggregate({
|
|
where: { year, month, status: PaymentStatus.PAYED },
|
|
_sum: { amountPayed: true },
|
|
}),
|
|
prisma.expense.aggregate({
|
|
where: { year, month, status: PaymentStatus.PENDING },
|
|
_sum: { amount: true },
|
|
}),
|
|
]);
|
|
return {
|
|
payed: Number(payed._sum.amountPayed ?? 0),
|
|
pending: Number(pending._sum.amount ?? 0),
|
|
};
|
|
}
|
|
|
|
export async function getTotalPending() {
|
|
const result = await prisma.expense.aggregate({
|
|
where: { status: PaymentStatus.PENDING },
|
|
_sum: { amount: true },
|
|
});
|
|
return { total: Number(result._sum.amount ?? 0) };
|
|
}
|
|
|
|
export async function generateExpensesForCurrentMonth() {
|
|
const { year, month } = getCurrentYearMonthUTC();
|
|
|
|
const periodics = await prisma.periodicExpense.findMany({
|
|
where: { isDeleted: false, periods: { has: month } },
|
|
});
|
|
|
|
const results: Array<{ id: number; status: string }> = [];
|
|
for (const periodic of periodics) {
|
|
const existing = await prisma.expense.findFirst({
|
|
where: { periodicExpenseId: periodic.id, year, month },
|
|
});
|
|
if (existing) {
|
|
results.push({ id: periodic.id, status: "already_exists" });
|
|
continue;
|
|
}
|
|
const dueDate = buildDueDate(year, month, periodic.defaultDueDay);
|
|
await prisma.expense.create({
|
|
data: {
|
|
description: periodic.description,
|
|
periodicExpenseId: periodic.id,
|
|
year,
|
|
month,
|
|
amount: periodic.defaultAmount,
|
|
dueDate,
|
|
status: PaymentStatus.PENDING,
|
|
},
|
|
});
|
|
results.push({ id: periodic.id, status: "created" });
|
|
}
|
|
return results;
|
|
}
|