import type { Context } from "hono"; import { PaymentStatus } from "../../../generated/prisma/client"; import { cache } from "../../../lib/cache"; import { logger } from "../../../lib/logger"; import { prisma } from "../../../lib/prisma"; function parseAmount(raw: string): number | null { const cleaned = raw.replace("$", "").replace(/,/g, "").trim(); if (!cleaned) return null; const n = Number(cleaned); return Number.isNaN(n) ? null : n; } function parseDate(raw: string): Date | null { const trimmed = raw.trim(); if (!trimmed) return null; const d = new Date(trimmed); return Number.isNaN(d.getTime()) ? null : d; } export async function importExpensesHandler(c: Context) { const body = await c.req.parseBody(); const file = body.file; if (!file || !(file instanceof File)) { return c.json({ error: "No se envió ningún archivo" }, 400); } const text = await file.text(); const lines = text.trim().split("\n"); if (lines.length < 2) { return c.json( { error: "El archivo está vacío o solo tiene encabezados" }, 400, ); } let imported = 0; let skipped = 0; const errors: string[] = []; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const parts = line.split("|"); if (parts.length < 10) { errors.push( `Línea ${i + 1}: formato inválido (${parts.length} columnas)`, ); continue; } const description = parts[9].trim(); const year = Number(parts[2].trim()); const month = Number(parts[3].trim()); const rawAmount = parts[4].trim(); const rawAmountPayed = parts[5].trim(); const rawStatus = parts[6].trim(); const rawDueDate = parts[7].trim(); const rawPaymentDate = parts[8].trim(); try { const amount = parseAmount(rawAmount); if (amount === null) { errors.push(`Línea ${i + 1} ("${description}"): monto inválido`); continue; } const amountPayed = parseAmount(rawAmountPayed); const dueDate = parseDate(rawDueDate); const paymentDate = parseDate(rawPaymentDate); if (!dueDate) { errors.push( `Línea ${i + 1} ("${description}"): fecha de vencimiento inválida`, ); continue; } const existing = await prisma.expense.findFirst({ where: { description, year, month }, }); if (existing) { skipped++; continue; } let periodicExpenseId: number | null = null; if (parts[1].trim()) { const periodic = await prisma.periodicExpense.findFirst({ where: { description: { equals: description, mode: "insensitive" } }, }); if (periodic) { periodicExpenseId = periodic.id; } } const status = rawStatus === "PAYED" ? PaymentStatus.PAYED : PaymentStatus.PENDING; await prisma.expense.create({ data: { description, year, month, amount, amountPayed, status, dueDate, paymentDate, periodicExpenseId, }, }); imported++; } catch (err) { const message = err instanceof Error ? err.message : String(err); errors.push(`Línea ${i + 1} ("${description}"): ${message}`); logger.error({ err, description }, "Error importing expense"); } } cache.invalidateByPrefix("expenses:"); return c.json({ imported, skipped, errors: errors.length > 0 ? errors : undefined, }); }