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:
1 parent
3cb7532417
commit
1573931c0f
13 files changed
+2234
-5
No files matched your search
@@ -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 })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user