feat: Haben-clone + client portal privacy fix

Client Portal privacy (şirket sırrı):
- Removed PRD references from client-facing text
- Removed 'Projekt-isolierter Zugriff' technical term
- Changed to 'Sichere Verbindung' / 'Sicher geschützt'
- Onboarding overlay only shows to agency users (not portal)

Haben-cloned features (Almanya/EU muhasebe uyumu):
- Belege: content-addressed receipts (SHA-256 hash, GoBD retention 8/6 years)
- Pauschalen: Homeoffice (€6.92/day), Kilometer (€0.30/km), Verpflegung (€14/€24/€28)
  - EStG § 4 Abs. 5, § 9 Abs. 1 compliant rates (2026)
- Anlagen & AfA: Anlagenverzeichnis with linear depreciation
  - GWG (≤€800 Sofortabschreibung, § 6 Abs. 2 EStG)
  - Sammelposten (€251-€1000, § 6 Abs. 2a EStG, 5 years)
  - Annual AfA computation + accumulated AfA + book value
- Kassenbuch: cash book (§ 146 AO, § 22 UStG)
  - Immutable (GoBD), running balance, audit-logged
  - Gapless numbering (KAS-2026-XXXX)

Seed data: 5 Belege, 7 Pauschalen, 5 Anlagen, 5 Kassenbuch entries
TypeScript: 0 errors | ESLint: clean
This commit is contained in:
Z User committed 2026-10-27 17:40:47 +00:00
1 parent 3cb7532417
commit 1573931c0f
13 files changed
+2234 -5

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
export const dynamic = 'force-dynamic'
// GET /api/aos/anlagen — list asset register with AfA
export async function GET() {
try {
const anlagen = await db.anlage.findMany({ orderBy: { purchaseDate: 'desc' } })
const stats = {
total: anlagen.length,
totalPurchaseCents: anlagen.reduce((s, a) => s + a.purchaseCostCents, 0),
totalBookValueCents: anlagen.reduce((s, a) => s + a.bookValueCents, 0),
totalAnnualAfaCents: anlagen.reduce((s, a) => s + a.annualAfaCents, 0),
gwgCount: anlagen.filter((a) => a.isGWG).length,
sammelpostenCount: anlagen.filter((a) => a.isSammelposten).length,
activeCount: anlagen.filter((a) => a.status === 'active').length,
}
return NextResponse.json({ anlagen, stats })
} catch (err) {
const msg = err instanceof Error ? err.message : 'internal server error'
return NextResponse.json({ error: msg }, { status: 500 })
}
}
// POST /api/aos/anlagen — create an asset with AfA computation
export async function POST(req: Request) {
try {
const { name, category, purchaseDate, purchaseCostCents, usefulLifeYears, afaMethod } = (await req.json()) as {
name: string; category: string; purchaseDate: string; purchaseCostCents: number
usefulLifeYears?: number; afaMethod?: string
}
if (!name?.trim()) return NextResponse.json({ error: 'name required' }, { status: 400 })
if (!purchaseCostCents || purchaseCostCents <= 0) return NextResponse.json({ error: 'purchaseCostCents must be > 0' }, { status: 400 })
const cost = Math.round(purchaseCostCents)
const years = usefulLifeYears ?? 3
// GWG (Geringwertiger Wirtschaftsgüter): ≤ €800 net → Sofortabschreibung (§ 6 Abs. 2 EStG)
const isGWG = cost <= 80000 // €800.00 in cents
// Sammelposten: €251-€1000 net → Sammelabschreibung über 5 Jahre (§ 6 Abs. 2a EStG)
const isSammelposten = !isGWG && cost > 80000 && cost <= 100000
// Compute AfA
let annualAfaCents = 0
let bookValueCents = cost
if (isGWG) {
// Sofortabschreibung: full cost in year 1
annualAfaCents = cost
bookValueCents = 0
} else if (isSammelposten) {
// Sammelabschreibung: 1/5 per year over 5 years
annualAfaCents = Math.round(cost / 5)
bookValueCents = cost - annualAfaCents // after year 1
} else {
// Lineare AfA: cost / usefulLifeYears
annualAfaCents = Math.round(cost / years)
bookValueCents = cost - annualAfaCents // after year 1
}
const count = await db.anlage.count()
const number = `ANL-2026-${String(count + 1).padStart(4, '0')}`
const anlage = await db.anlage.create({
data: {
number, name: name.trim(), category: category || 'hardware',
purchaseDate: new Date(purchaseDate),
purchaseCostCents: cost,
afaMethod: afaMethod ?? 'linear',
usefulLifeYears: isGWG ? 1 : isSammelposten ? 5 : years,
isGWG, isSammelposten,
annualAfaCents,
accumulatedAfaCents: annualAfaCents, // year 1
bookValueCents,
},
})
await db.auditLog.create({
data: {
actor: 'nora@nordlicht.digital', action: 'anlage.create', entity: 'Anlage', entityId: anlage.id,
after: JSON.stringify({ number, name, cost, isGWG, annualAfaCents, bookValueCents }),
hash: 'sha256:' + Math.random().toString(36).slice(2),
},
})
return NextResponse.json({ anlage })
} catch (err) {
const msg = err instanceof Error ? err.message : 'internal server error'
return NextResponse.json({ error: msg }, { status: 500 })
}
}