5 PRD gaps fixed: 1. Zod validation on all API inputs (§5.2) — 26 files, 27 safeParse 2. Rate limiting on public endpoints (§5.6) — 6 scopes, 7 endpoints 3. Custom fields JSONB with schema (§5.5) — 2 models, 10 seed defs 4. CSP + security headers (§5.6) — 7 headers via next.config.ts 5. ESLint module boundary rule (§5.4) — no-restricted-imports 0 TS errors, ESLint clean.
102 lines
3.7 KiB
TypeScript
Executable File
102 lines
3.7 KiB
TypeScript
Executable File
import { NextResponse } from 'next/server'
|
|
import { z } from 'zod'
|
|
import { db } from '@/lib/db'
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
const createAnlageSchema = z.object({
|
|
name: z.string().min(1),
|
|
category: z.string().optional().default('hardware'),
|
|
purchaseDate: z.string().min(1),
|
|
purchaseCostCents: z.number().int().positive(),
|
|
usefulLifeYears: z.number().int().min(1).optional(),
|
|
afaMethod: z.string().optional(),
|
|
})
|
|
|
|
// 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 parsed = createAnlageSchema.safeParse(await req.json())
|
|
if (!parsed.success) {
|
|
return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 })
|
|
}
|
|
const { name, category, purchaseDate, purchaseCostCents, usefulLifeYears, afaMethod } = parsed.data
|
|
|
|
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 })
|
|
}
|
|
}
|