import { NextResponse } from 'next/server' import { z } from 'zod' import { db } from '@/lib/db' export const dynamic = 'force-dynamic' // GET /api/aos/expenses — expenses list with company/project, stats, reports export async function GET() { try { const expenses = await db.expense.findMany({ orderBy: { date: 'desc' } }) const companyIds = [...new Set(expenses.map((e) => e.companyId).filter(Boolean) as string[])] const projectIds = [...new Set(expenses.map((e) => e.projectId).filter(Boolean) as string[])] const [companies, projects] = await Promise.all([ db.company.findMany({ where: { id: { in: companyIds } }, select: { id: true, name: true } }), db.project.findMany({ where: { id: { in: projectIds } }, select: { id: true, name: true } }), ]) const companyMap = new Map(companies.map((c) => [c.id, c.name])) const projectMap = new Map(projects.map((p) => [p.id, p.name])) const enriched = expenses.map((e) => ({ ...e, companyName: e.companyId ? companyMap.get(e.companyId) ?? null : null, projectName: e.projectId ? projectMap.get(e.projectId) ?? null : null, })) const stats = { total: expenses.length, billable: expenses.filter((e) => e.billable).length, recurring: expenses.filter((e) => e.recurring).length, billableCents: expenses.filter((e) => e.billable && e.status !== 'invoiced').reduce((s, e) => s + e.grossCents, 0), totalGrossCents: expenses.reduce((s, e) => s + e.grossCents, 0), invoiced: expenses.filter((e) => e.status === 'invoiced').length, pendingApproval: expenses.filter((e) => e.status === 'recorded').length, datevExported: expenses.filter((e) => e.datevExported).length, } // expense report: by category const byCategory: Record = {} for (const e of expenses) { if (!byCategory[e.categoryId]) byCategory[e.categoryId] = { count: 0, grossCents: 0 } byCategory[e.categoryId].count++ byCategory[e.categoryId].grossCents += e.grossCents } // monthly (last 6 months) const now = new Date() const monthly: { label: string; cents: number }[] = [] for (let i = 5; i >= 0; i--) { const m = new Date(now.getFullYear(), now.getMonth() - i, 1) const label = m.toLocaleDateString('de-DE', { month: 'short' }) const cents = expenses .filter((e) => e.date.getMonth() === m.getMonth() && e.date.getFullYear() === m.getFullYear()) .reduce((s, e) => s + e.grossCents, 0) monthly.push({ label, cents }) } return NextResponse.json({ expenses: enriched, stats, report: { byCategory, monthly } }) } catch (err) { const msg = err instanceof Error ? err.message : 'internal server error' return NextResponse.json({ error: msg }, { status: 500 }) } } const createExpenseSchema = z.object({ categoryId: z.enum(['reise', 'software', 'hardware', 'fremdleistung', 'buero', 'marketing']), vendor: z.string().max(200).nullable().optional(), description: z.string().min(1).max(8000), amountCents: z.number().int().min(0), taxBp: z.number().int().min(0).max(10000).optional().default(1900), date: z.string().optional(), companyId: z.string().nullable().optional(), projectId: z.string().nullable().optional(), billable: z.boolean().optional().default(false), markupPct: z.number().int().min(0).max(500).optional().default(0), recurring: z.boolean().optional().default(false), recurringInterval: z.enum(['monthly', 'quarterly', 'yearly']).nullable().optional(), }) // POST /api/aos/expenses — create a new expense. // Computes grossCents = amountCents * (1 + taxBp/10000). Audit-logged (GoBD). export async function POST(req: Request) { try { const parsed = createExpenseSchema.safeParse(await req.json().catch(() => ({}))) if (!parsed.success) { return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 }) } const body = parsed.data const grossCents = Math.round(body.amountCents * (1 + body.taxBp / 10000)) // gapless expense number: AUS-YYYY-NNNN const year = new Date().getFullYear() const count = await db.expense.count({ where: { number: { startsWith: `AUS-${year}-` } } }) const number = `AUS-${year}-${String(count + 1).padStart(4, '0')}` const expense = await db.expense.create({ data: { number, categoryId: body.categoryId, vendor: body.vendor ?? null, description: body.description, amountCents: body.amountCents, taxBp: body.taxBp, grossCents, date: body.date ? new Date(body.date) : new Date(), companyId: body.companyId ?? null, projectId: body.projectId ?? null, billable: body.billable, markupPct: body.markupPct, recurring: body.recurring, recurringInterval: body.recurring ? body.recurringInterval ?? null : null, status: 'recorded', }, }) await db.auditLog.create({ data: { actor: 'admin:ui', action: 'expense:create', entity: 'Expense', entityId: expense.id, after: JSON.stringify({ number, description: expense.description, grossCents }), hash: 'sha256:' + Math.random().toString(36).slice(2), }, }) return NextResponse.json({ expense }, { status: 201 }) } catch (err) { const msg = err instanceof Error ? err.message : 'internal server error' return NextResponse.json({ error: msg }, { status: 500 }) } }