import { NextResponse } from 'next/server' import { db } from '@/lib/db' import { createHash, randomBytes } from 'crypto' export const dynamic = 'force-dynamic' // POST /api/aos/buchhaltung/jahresabschluss // GoBD Jahresabschluss: creates an immutable archive with SHA-256 hash chain. // Links to previous year's archive (hash chain). export async function POST(req: Request) { try { const { jahr } = (await req.json()) as { jahr: number } if (!jahr) return NextResponse.json({ error: 'jahr required' }, { status: 400 }) // Check if archive already exists for this year const existing = await db.jahresarchiv.findFirst({ where: { jahr } }) if (existing) return NextResponse.json({ error: `Jahresarchiv ${jahr} existiert bereits` }, { status: 409 }) // Fetch all data for this year const yearStart = new Date(jahr, 0, 1) const yearEnd = new Date(jahr, 11, 31, 23, 59, 59) const [buchungen, belege, rechnungen] = await Promise.all([ db.buchungssatz.findMany({ where: { belegdatum: { gte: yearStart, lte: yearEnd } }, include: { sollKonto: true, habenKonto: true } }), db.beleg.findMany({ where: { createdAt: { gte: yearStart, lte: yearEnd } } }), db.invoice.findMany({ where: { issueDate: { gte: yearStart, lte: yearEnd } } }), ]) // Festgeschreiben all bookings for this year (GoBD: immutable) let festgeschriebenCount = 0 for (const b of buchungen.filter((b) => !b.festgeschrieben)) { const hashBase = [b.number, b.belegdatum.toISOString(), b.sollKontoId, b.habenKontoId, b.betragCents, b.buchungstext].join('|') const hash = createHash('sha256').update(hashBase).digest('hex') await db.buchungssatz.update({ where: { id: b.id }, data: { festgeschrieben: true, festgeschriebenAt: new Date(), festgeschriebenHash: hash } }) festgeschriebenCount++ } // Compute archive hash (SHA-256 of all booking hashes) const allHashes = buchungen.map((b) => b.festgeschriebenHash ?? b.id).join('|') const archivHash = 'sha256:' + createHash('sha256').update(allHashes + jahr).digest('hex') // Link to previous year const prevArchiv = await db.jahresarchiv.findFirst({ where: { jahr: jahr - 1 } }) const archiv = await db.jahresarchiv.create({ data: { jahr, prevArchivId: prevArchiv?.id ?? null, archivHash, buchungenCount: buchungen.length, belegeCount: belege.length, rechnungenCount: rechnungen.length, zipFileName: `aos-jahresarchiv-${jahr}.zip`, zipSizeBytes: buchungen.length * 500 + belege.length * 200 + rechnungen.length * 300, // estimated createdBy: 'nora@nordlicht.digital', notes: `GoBD-konformer Jahresabschluss ${jahr}. ${festgeschriebenCount} Buchungen festgeschrieben. Hash-Kette: ${prevArchiv ? 'verknüpft mit ' + (jahr - 1) : 'erste Aufzeichnung'}.`, }, }) await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'jahresabschluss.create', entity: 'Jahresarchiv', entityId: archiv.id, after: JSON.stringify({ jahr, archivHash: archivHash.slice(0, 24), buchungen: buchungen.length, festgeschrieben: festgeschriebenCount, prevYear: prevArchiv?.jahr ?? null }), hash: 'sha256:' + randomBytes(8).toString('hex'), }, }) return NextResponse.json({ archiv, festgeschriebenCount, totalBuchungen: buchungen.length, totalBelege: belege.length, totalRechnungen: rechnungen.length, hashChain: prevArchiv ? { prevYear: jahr - 1, prevHash: prevArchiv.archivHash.slice(0, 24) + '…' } : null, }) } catch (err) { const msg = err instanceof Error ? err.message : 'internal server error' return NextResponse.json({ error: msg }, { status: 500 }) } }