102a8603-26df-462f-a56d-01cee292f6f6
This commit is contained in:
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 })
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user