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) { 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) { 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 = {}; 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) { 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) { 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; }