102a8603-26df-462f-a56d-01cee292f6f6

This commit is contained in:
Z User committed 2026-10-26 16:00:34 +00:00
1 parent 40805e16cd
commit 85c4006803
19 files changed
+987

No files matched your search

@@ -0,0 +1,48 @@
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
export const dynamic = 'force-dynamic'
// POST /api/aos/accounting/bank/[id]/match — match a bank transaction to an invoice.
// Body: { invoiceId } — marks the transaction matched + invoice paid.
export async function POST(
req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const { invoiceId } = (await req.json()) as { invoiceId: string }
const tx = await db.bankTransaction.findUnique({ where: { id } })
if (!tx) return NextResponse.json({ error: 'transaction not found' }, { status: 404 })
const invoice = await db.invoice.findUnique({ where: { id: invoiceId } })
if (!invoice) return NextResponse.json({ error: 'invoice not found' }, { status: 404 })
const updated = await db.bankTransaction.update({
where: { id },
data: {
matchedInvoiceId: invoiceId,
matchedStatus: 'matched',
matchReason: 'Manuell zugeordnet',
},
})
// mark invoice as paid
await db.invoice.update({
where: { id: invoiceId },
data: { status: 'paid', paidDate: tx.valueDate },
})
await db.auditLog.create({
data: {
actor: 'nora@nordlicht.digital',
action: 'bank.match:invoice',
entity: 'BankTransaction',
entityId: id,
after: JSON.stringify({ invoiceId, invoiceNumber: invoice.number, status: 'matched', invoiceStatus: 'paid' }),
hash: 'sha256:' + Math.random().toString(36).slice(2),
},
})
return NextResponse.json({ transaction: updated, invoiceNumber: invoice.number })
}
+84
View File
@@ -0,0 +1,84 @@
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
export const dynamic = 'force-dynamic'
// GET /api/aos/accounting — accounting overview:
// - VAT returns (Umsatzsteuer-Voranmeldung) with Kennzahlen
// - EÜR (Einnahmen-Überschuss-Rechnung) from invoices + expenses
// - Bank transactions (CAMT.053 import) with match status
// - Dunning (Mahnwesen) for overdue invoices
export async function GET() {
const [vatReturns, bankTx, invoices, expenses, dunning] = await Promise.all([
db.vatReturn.findMany({ orderBy: [{ periodYear: 'desc' }, { periodMonth: 'desc' }] }),
db.bankTransaction.findMany({ orderBy: { bookingDate: 'desc' } }),
db.invoice.findMany({ where: { status: { in: ['paid', 'sent', 'overdue'] } }, include: { company: true } }),
db.expense.findMany(),
db.dunningRun.findMany({ orderBy: { sentAt: 'desc' }, include: { invoice: { include: { company: true } } } }),
])
// --- VAT stats ---
const currentVat = vatReturns.find((v) => v.status === 'computed' || v.status === 'draft')
const vatHistory = vatReturns.filter((v) => v.status === 'accepted')
const totalPayableYTD = vatReturns.filter((v) => v.status === 'accepted').reduce((s, v) => s + v.payableCents, 0)
// --- EÜR (income statement) ---
// Income = paid invoices (net), Cost = expenses (net, non-billable internal)
const now = new Date()
const eurMonths: { label: string; incomeCents: number; costCents: number; profitCents: number }[] = []
for (let i = 5; i >= 0; i--) {
const m = new Date(now.getFullYear(), now.getMonth() - i, 1)
const label = m.toLocaleDateString('de-DE', { month: 'short' })
const incomeCents = invoices
.filter((inv) => inv.status === 'paid' && inv.paidDate && inv.paidDate.getMonth() === m.getMonth() && inv.paidDate.getFullYear() === m.getFullYear())
.reduce((s, i) => s + i.netCents, 0)
const costCents = expenses
.filter((e) => e.date.getMonth() === m.getMonth() && e.date.getFullYear() === m.getFullYear())
.reduce((s, e) => s + e.amountCents, 0)
eurMonths.push({ label, incomeCents, costCents, profitCents: incomeCents - costCents })
}
const totalIncome = eurMonths.reduce((s, m) => s + m.incomeCents, 0)
const totalCost = eurMonths.reduce((s, m) => s + m.costCents, 0)
const totalProfit = totalIncome - totalCost
// EÜR by category (costs)
const costsByCategory: Record<string, number> = {}
for (const e of expenses) {
costsByCategory[e.categoryId] = (costsByCategory[e.categoryId] ?? 0) + e.amountCents
}
// --- Bank reconciliation ---
const matched = bankTx.filter((t) => t.matchedStatus === 'matched').length
const suggested = bankTx.filter((t) => t.matchedStatus === 'suggested').length
const unmatched = bankTx.filter((t) => t.matchedStatus === 'unmatched').length
const inflowCents = bankTx.filter((t) => t.amountCents > 0).reduce((s, t) => s + t.amountCents, 0)
const outflowCents = bankTx.filter((t) => t.amountCents < 0).reduce((s, t) => s + t.amountCents, 0)
// --- Dunning ---
const overdueInvoices = invoices.filter((i) => i.status === 'overdue')
return NextResponse.json({
vat: {
current: currentVat,
history: vatHistory,
totalPayableYTD,
stats: { periods: vatReturns.length, accepted: vatHistory.length, pending: currentVat ? 1 : 0 },
},
eur: {
months: eurMonths,
totalIncome,
totalCost,
totalProfit,
costsByCategory: Object.entries(costsByCategory).map(([cat, cents]) => ({ category: cat, cents })).sort((a, b) => b.cents - a.cents),
},
bank: {
transactions: bankTx,
stats: { matched, suggested, unmatched, inflowCents, outflowCents, total: bankTx.length },
},
dunning: {
overdue: overdueInvoices.map((i) => ({ id: i.id, number: i.number, companyName: i.company.name, grossCents: i.grossCents, dueDate: i.dueDate, daysOverdue: Math.round((Date.now() - i.dueDate.getTime()) / 86400000) })),
runs: dunning,
stats: { overdueCount: overdueInvoices.length, overdueCents: overdueInvoices.reduce((s, i) => s + i.grossCents, 0), dunningSent: dunning.length },
},
})
}
@@ -0,0 +1,44 @@
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
export const dynamic = 'force-dynamic'
// PATCH /api/aos/accounting/vat/[id]/submit — submit VAT return to ELSTER (mock ERiC).
// Transitions computed → submitted → accepted, generates elsterRef, writes audit.
export async function PATCH(
_req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const vat = await db.vatReturn.findUnique({ where: { id } })
if (!vat) return NextResponse.json({ error: 'not found' }, { status: 404 })
if (vat.status === 'accepted') return NextResponse.json({ error: 'already accepted' }, { status: 409 })
const monthName = new Date(vat.periodYear, vat.periodMonth - 1, 1).toLocaleDateString('de-DE', { month: 'short' })
const elsterRef = `ERiC-${vat.periodYear}-${String(vat.periodMonth).padStart(2, '0')}-NORDLICHT-${Math.floor(Math.random() * 9000 + 1000)}`
const updated = await db.vatReturn.update({
where: { id },
data: {
status: 'accepted',
submittedAt: new Date(),
acceptedAt: new Date(),
elsterRef,
notes: `Umsatzsteuer-Voranmeldung ${monthName} ${vat.periodYear} via ERiC übermittelt und vom Finanzamt bestätigt.`,
},
})
await db.auditLog.create({
data: {
actor: 'nora@nordlicht.digital',
action: 'vat.submit:elster',
entity: 'VatReturn',
entityId: id,
before: JSON.stringify({ status: vat.status }),
after: JSON.stringify({ status: 'accepted', elsterRef, payableCents: vat.payableCents }),
hash: 'sha256:' + Math.random().toString(36).slice(2),
},
})
return NextResponse.json({ vat: updated, elsterRef })
}
+3
View File
@@ -15,6 +15,9 @@ export async function POST() {
try {
// wipe via prisma (SQLite TRUNCATE not supported, use deleteMany per model in order)
const { db } = await import('@/lib/db')
await db.dunningRun.deleteMany()
await db.vatReturn.deleteMany()
await db.bankTransaction.deleteMany()
await db.estimateLine.deleteMany()
await db.estimate.deleteMany()
await db.ticketMessage.deleteMany()