feat: Full company accounting — SKR04, doppelte Buchführung, GoBD Festschreibung

Haben-cloned features for complete company accounting:

SKR04 Kontenrahmen (21 Konten):
- Bestandskonten (Aktiv/Passiv): Bank, Kasse, EDV-Anlagen, Verbindlichkeiten
- Erlöskonten: Erlöse 19% USt, Fremdleistungen
- Aufwandskonten: Bezogene Leistungen, Bürobedarf, EDV-Kosten, Reisekosten, Löhne
- Steuerkonten: USt 19%/7%, abziehbare Vorsteuer

Doppelte Buchführung (Buchungssatz):
- Soll an Haben booking system
- Journal with lückenlose Nummerierung (BUC-2026-XXXX)
- Saldenliste (account balances per Konto)
- GoBD Festschreibung (immutable via SHA-256 hash)
- Reverse Charge and §19 UStG (Kleinunternehmer) flags

EÜR (Einnahmen-Überschuss-Rechnung):
- Anlage EÜR with AVEÜR line numbers
- § 4 Abs. 3 EStG computation
- Gewinn: €14.506 (2026)

Jahresabschluss (GoBD):
- Immutable year-end archive with SHA-256 hash chain
- Links to previous year (2025 → 2026)
- Festschreibung of all bookings in the year
- Aufbewahrungsfrist: 8 Jahre (GoBD) / 10 Jahre (§ 147 AO)

Jahreserklärungen:
- USt-Erklärung (yearly, more lines than monthly UStVA)
- Anlage EÜR, Einkommensteuer, Gewerbesteuer
- ELSTER submission tracking

Seed: 21 Konten, 7 Buchungssätze, 9 EÜR entries, 2 Jahreserklärungen, 1 Jahresarchiv
This commit is contained in:
Z User committed 2026-10-27 18:13:04 +00:00
1 parent 0d0fa6b5e1
commit 6cbf0b2879
6 files changed
+613

No files matched your search

@@ -0,0 +1,81 @@
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 })
}
}
+150
View File
@@ -0,0 +1,150 @@
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { createHash } from 'crypto'
export const dynamic = 'force-dynamic'
// GET /api/aos/buchhaltung — SKR04 Konten + Journal + Saldenliste + EÜR + Jahresabschluss
export async function GET() {
try {
const [konten, buchungen, euerEntries, jahreserkl, archiv] = await Promise.all([
db.konto.findMany({ orderBy: { nummer: 'asc' } }),
db.buchungssatz.findMany({
orderBy: { belegdatum: 'desc' },
include: { sollKonto: true, habenKonto: true },
}),
db.euerEntry.findMany({ where: { jahr: 2026 }, orderBy: { zeile: 'asc' } }),
db.jahreserklaerung.findMany({ orderBy: { jahr: 'desc' } }),
db.jahresarchiv.findMany({ orderBy: { jahr: 'desc' } }),
])
// Saldenliste (account balances)
const salden = konten.map((k) => {
const sollSum = buchungen.filter((b) => b.sollKontoId === k.id).reduce((s, b) => s + b.betragCents, 0)
const habenSum = buchungen.filter((b) => b.habenKontoId === k.id).reduce((s, b) => s + b.betragCents, 0)
const saldo = k.art === 'aktiv' || k.art === 'aufwand' ? sollSum - habenSum : habenSum - sollSum
return { konto: k.nummer, name: k.name, art: k.art, saldoCents: saldo, buchungen: buchungen.filter((b) => b.sollKontoId === k.id || b.habenKontoId === k.id).length }
}).filter((s) => s.saldoCents !== 0 || s.buchungen > 0)
// EÜR totals
const einnahmen = euerEntries.filter((e) => e.art === 'einnahme').reduce((s, e) => s + e.betragCents, 0)
const ausgaben = euerEntries.filter((e) => e.art === 'ausgabe').reduce((s, e) => s + e.betragCents, 0)
const gewinn = einnahmen - ausgaben
// Festschreibung stats
const festgeschrieben = buchungen.filter((b) => b.festgeschrieben).length
const offene = buchungen.filter((b) => !b.festgeschrieben).length
return NextResponse.json({
konten,
journal: buchungen.map((b) => ({
number: b.number, belegdatum: b.belegdatum, buchungstext: b.buchungstext,
betragCents: b.betragCents, belegNummer: b.belegNummer,
sollKonto: b.sollKonto?.nummer ?? '?', sollName: b.sollKonto?.name ?? '?',
habenKonto: b.habenKonto?.nummer ?? '?', habenName: b.habenKonto?.name ?? '?',
festgeschrieben: b.festgeschrieben, festgeschriebenHash: b.festgeschriebenHash,
reverseCharge: b.reverseCharge, par19UStG: b.par19UStG,
})),
salden,
euer: { entries: euerEntries, einnahmen, ausgaben, gewinn },
jahreserkl,
archiv,
stats: {
kontenCount: konten.length,
buchungenCount: buchungen.length,
festgeschrieben,
offene,
euerEintraege: euerEntries.length,
gewinn,
jahreserklCount: jahreserkl.length,
archivCount: archiv.length,
},
})
} catch (err) {
const msg = err instanceof Error ? err.message : 'internal server error'
return NextResponse.json({ error: msg }, { status: 500 })
}
}
// POST /api/aos/buchhaltung/buchung — create a Buchungssatz (Soll an Haben)
export async function POST(req: Request) {
try {
const body = (await req.json()) as {
belegdatum: string
sollKontoNummer: string
habenKontoNummer: string
betragCents: number
buchungstext: string
belegNummer?: string
steuerBp?: number
reverseCharge?: boolean
par19UStG?: boolean
}
const sollKonto = await db.konto.findUnique({ where: { nummer: body.sollKontoNummer } })
const habenKonto = await db.konto.findUnique({ where: { nummer: body.habenKontoNummer } })
if (!sollKonto) return NextResponse.json({ error: `Soll-Konto ${body.sollKontoNummer} nicht gefunden` }, { status: 422 })
if (!habenKonto) return NextResponse.json({ error: `Haben-Konto ${body.habenKontoNummer} nicht gefunden` }, { status: 422 })
const count = await db.buchungssatz.count()
const number = `BUC-2026-${String(count + 1).padStart(4, '0')}`
const buchung = await db.buchungssatz.create({
data: {
number, belegdatum: new Date(body.belegdatum),
sollKontoId: sollKonto.id, habenKontoId: habenKonto.id,
betragCents: Math.round(body.betragCents),
buchungstext: body.buchungstext,
belegNummer: body.belegNummer ?? null,
steuerBp: body.steuerBp ?? null,
reverseCharge: body.reverseCharge ?? false,
par19UStG: body.par19UStG ?? false,
},
})
await db.auditLog.create({
data: {
actor: 'nora@nordlicht.digital', action: 'buchung.create', entity: 'Buchungssatz', entityId: buchung.id,
after: JSON.stringify({ number, soll: body.sollKontoNummer, haben: body.habenKontoNummer, betrag: body.betragCents }),
hash: 'sha256:' + Math.random().toString(36).slice(2),
},
})
return NextResponse.json({ buchung })
} catch (err) {
const msg = err instanceof Error ? err.message : 'internal server error'
return NextResponse.json({ error: msg }, { status: 500 })
}
}
// PATCH — Festschreibung (GoBD: make immutable)
export async function PATCH(req: Request) {
try {
const { buchungId } = (await req.json()) as { buchungId: string }
const buchung = await db.buchungssatz.findUnique({ where: { id: buchungId } })
if (!buchung) return NextResponse.json({ error: 'not found' }, { status: 404 })
if (buchung.festgeschrieben) return NextResponse.json({ error: 'already festgeschrieben' }, { status: 409 })
// GoBD Festschreibung: compute hash of the booking (immutable)
const hashBase = [buchung.number, buchung.belegdatum.toISOString(), buchung.sollKontoId, buchung.habenKontoId, buchung.betragCents, buchung.buchungstext].join('|')
const festgeschriebenHash = createHash('sha256').update(hashBase).digest('hex')
const updated = await db.buchungssatz.update({
where: { id: buchungId },
data: { festgeschrieben: true, festgeschriebenAt: new Date(), festgeschriebenHash },
})
await db.auditLog.create({
data: {
actor: 'nora@nordlicht.digital', action: 'buchung.festschreiben', entity: 'Buchungssatz', entityId: buchungId,
after: JSON.stringify({ hash: festgeschriebenHash }),
hash: 'sha256:' + Math.random().toString(36).slice(2),
},
})
return NextResponse.json({ buchung: updated, hash: festgeschriebenHash })
} catch (err) {
const msg = err instanceof Error ? err.message : 'internal server error'
return NextResponse.json({ error: msg }, { status: 500 })
}
}
+5
View File
@@ -27,6 +27,11 @@ export async function POST() {
await db.anlage.deleteMany()
await db.pauschale.deleteMany()
await db.beleg.deleteMany()
await db.jahresarchiv.deleteMany()
await db.jahreserklaerung.deleteMany()
await db.euerEntry.deleteMany()
await db.buchungssatz.deleteMany()
await db.konto.deleteMany()
await db.estimateLine.deleteMany()
await db.estimate.deleteMany()
await db.ticketMessage.deleteMany()