Files
AOS/src/app/api/aos/anlagen/route.ts
T
Z User 7acc0c5451 chore: sync files after PRD gap fixes (Zod, rate-limit, custom-fields, CSP, boundary)
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.
2026-10-27 22:48:01 +00:00

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 })
}
}