import { eq, and, gte, lte, isNull, or, ilike } from 'drizzle-orm'; import { db } from '@/lib/db'; import { expenses } from '@/lib/db/schema/financial'; import { generatePdf } from '@/lib/pdf/generate'; import { getRate } from '@/lib/services/currency'; import { logger } from '@/lib/logger'; import type { ListExpensesInput } from '@/lib/validators/expenses'; async function fetchAllExpenses(portId: string, query: ListExpensesInput) { const conditions: ReturnType[] = [eq(expenses.portId, portId) as any]; if (!query.includeArchived) { conditions.push(isNull(expenses.archivedAt) as any); } if (query.category) { conditions.push(eq(expenses.category, query.category) as any); } if (query.paymentStatus) { conditions.push(eq(expenses.paymentStatus, query.paymentStatus) as any); } if (query.currency) { conditions.push(eq(expenses.currency, query.currency) as any); } if (query.payer) { conditions.push(eq(expenses.payer, query.payer) as any); } if (query.dateFrom) { conditions.push(gte(expenses.expenseDate, new Date(query.dateFrom)) as any); } if (query.dateTo) { conditions.push(lte(expenses.expenseDate, new Date(query.dateTo)) as any); } if (query.search) { conditions.push( or( ilike(expenses.establishmentName, `%${query.search}%`), ilike(expenses.description, `%${query.search}%`), ) as any, ); } return db .select() .from(expenses) .where(and(...(conditions as any[]))); } export async function exportCsv(portId: string, query: ListExpensesInput): Promise { const rows = await fetchAllExpenses(portId, query); const headers = [ 'Date', 'Establishment', 'Category', 'Amount', 'Currency', 'Amount USD', 'Payment Status', 'Payment Method', 'Description', ]; const csvRows = rows.map((r) => { const date = r.expenseDate ? new Date(r.expenseDate).toISOString().split('T')[0] : ''; return [ date, r.establishmentName ?? '', r.category ?? '', r.amount, r.currency, r.amountUsd ?? 'N/A', r.paymentStatus ?? '', r.paymentMethod ?? '', (r.description ?? '').replace(/"/g, '""'), ] .map((v) => `"${v}"`) .join(','); }); return [headers.join(','), ...csvRows].join('\n'); } export async function exportPdf(portId: string, query: ListExpensesInput): Promise { const rows = await fetchAllExpenses(portId, query); const template = { basePdf: { width: 210, height: 297, padding: [10, 10, 10, 10] }, schemas: [ [ { name: 'title', type: 'text', position: { x: 10, y: 10 }, width: 190, height: 10, fontSize: 14, fontColor: '#000000', }, { name: 'content', type: 'text', position: { x: 10, y: 25 }, width: 190, height: 260, fontSize: 8, fontColor: '#000000', }, ], ], }; const lines = rows.map((r) => { const date = r.expenseDate ? new Date(r.expenseDate).toISOString().split('T')[0] : ''; return `${date} | ${r.establishmentName ?? '-'} | ${r.category ?? '-'} | ${r.amount} ${r.currency} | ${r.paymentStatus ?? '-'}`; }); const inputs = [ { title: 'Expense Report', content: lines.join('\n'), }, ]; return generatePdf(template as any, inputs); } export async function exportParentCompany( portId: string, query: ListExpensesInput, ): Promise { // BR-043: Convert all amounts to EUR, add 5% management fee const rows = await fetchAllExpenses(portId, query); const eurRate = await getRate('USD', 'EUR'); if (!eurRate) { logger.warn('EUR rate unavailable for parent company export, using 1:1 fallback'); } const rate = eurRate ?? 1; const convertedRows = rows.map((r) => { const amountUsd = r.amountUsd ? Number(r.amountUsd) : Number(r.amount); const amountEur = Number((amountUsd * rate).toFixed(2)); return { date: r.expenseDate ? new Date(r.expenseDate).toISOString().split('T')[0] : '', establishment: r.establishmentName ?? '-', category: r.category ?? '-', amountEur, }; }); const subtotal = convertedRows.reduce((sum, r) => sum + r.amountEur, 0); const fee = Number((subtotal * 0.05).toFixed(2)); const total = Number((subtotal + fee).toFixed(2)); const template = { basePdf: { width: 210, height: 297, padding: [10, 10, 10, 10] }, schemas: [ [ { name: 'title', type: 'text', position: { x: 10, y: 10 }, width: 190, height: 10, fontSize: 14, fontColor: '#000000', }, { name: 'content', type: 'text', position: { x: 10, y: 25 }, width: 190, height: 230, fontSize: 8, fontColor: '#000000', }, { name: 'summary', type: 'text', position: { x: 10, y: 260 }, width: 190, height: 30, fontSize: 10, fontColor: '#000000', }, ], ], }; const lines = convertedRows.map( (r) => `${r.date} | ${r.establishment} | ${r.category} | EUR ${r.amountEur.toFixed(2)}`, ); const summary = [ `Subtotal: EUR ${subtotal.toFixed(2)}`, `Management Fee (5%): EUR ${fee.toFixed(2)}`, `Total: EUR ${total.toFixed(2)}`, ].join('\n'); const inputs = [ { title: 'Parent Company Expense Report (EUR)', content: lines.join('\n'), summary, }, ]; return generatePdf(template as any, inputs); }