Bericht: vollstaendige Fehlerabsicherung + robustes Laden von pdfkit
Deploy App / deploy (push) Successful in 1m2s
Deploy App / deploy (push) Successful in 1m2s
Der gemeldete 500 kam OHNE JSON-Koerper -- der Client konnte deshalb nur "Fehler 500" zeigen. Ursache: Der Absturz lag ausserhalb der bisherigen try/catch-Bloecke. Jetzt liegt der GESAMTE Handler in einem try/catch, jede Ursache wird protokolliert und im Klartext zurueckgegeben. Zusaetzlich zwei Risiken entfernt: - Die verschachtelte Abfrage (Plan -> Szenarien -> Plan -> Personen) war ein Ringbezug; Szenarien werden jetzt einzeln nachgeladen. - pdfkit wird ueber einen Resolver geholt, der statischen Import, .default und createRequire durchprobiert -- unabhaengig davon, wie der Bundler CJS-Interop aufloest. Neuer Test mit realistischem Plan (Immobilie, Schuld, AHV, PK, 3a, Kopie-Szenario) -- laeuft lokal durch, schliesst die Datenform als Ursache aus. 223 Tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -67,86 +67,88 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
if (!parsed.success) return NextResponse.json({ error: "Ungültige Eingabe." }, { status: 400 });
|
||||
const cfg = parsed.data;
|
||||
|
||||
const plan = await prisma.plan.findFirst({
|
||||
where: { id: planId, userId },
|
||||
include: {
|
||||
persons: { orderBy: { role: "asc" } },
|
||||
scenarios: { orderBy: [{ isBase: "desc" }, { createdAt: "asc" }], include: planInclude },
|
||||
actuals: { orderBy: [{ year: "asc" }, { recordedOn: "asc" }] },
|
||||
},
|
||||
});
|
||||
if (!plan) return NextResponse.json({ error: "Plan nicht gefunden." }, { status: 404 });
|
||||
|
||||
// Nur Szenarien dieses Plans, in der Reihenfolge der Auswahl -- Basis zuerst.
|
||||
const chosen = cfg.scenarioIds
|
||||
.map((id) => plan.scenarios.find((s) => s.id === id))
|
||||
.filter((s): s is (typeof plan.scenarios)[number] => !!s)
|
||||
.sort((a, b) => (a.isBase === b.isBase ? 0 : a.isBase ? -1 : 1));
|
||||
if (chosen.length === 0) {
|
||||
return NextResponse.json({ error: "Kein gültiges Szenario ausgewählt." }, { status: 400 });
|
||||
}
|
||||
|
||||
const analyses =
|
||||
cfg.analysisIds.length === 0
|
||||
? []
|
||||
: await prisma.savedAnalysis.findMany({
|
||||
where: { id: { in: cfg.analysisIds }, planId },
|
||||
orderBy: { createdAt: "asc" },
|
||||
select: { name: true, type: true, result: true },
|
||||
});
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: userId }, select: { username: true } });
|
||||
|
||||
const config: ReportConfig = {
|
||||
title: cfg.title,
|
||||
metric: cfg.metric,
|
||||
source: cfg.source,
|
||||
scenarioIds: chosen.map((s) => s.id),
|
||||
analysisIds: cfg.analysisIds,
|
||||
comment: cfg.comment ?? null,
|
||||
};
|
||||
|
||||
const model = buildReport({
|
||||
planName: plan.name,
|
||||
author: user?.username ?? "unbekannt",
|
||||
createdAt: new Date(),
|
||||
config,
|
||||
scenarios: chosen.map((s) => ({ name: s.name, isBase: s.isBase, plan: toPlanInput(s) })),
|
||||
household: {
|
||||
householdType: plan.householdType,
|
||||
startYear: plan.startYear,
|
||||
persons: plan.persons.map((p) => ({ name: p.name, age: p.age, role: p.role })),
|
||||
},
|
||||
actuals: plan.actuals.map(
|
||||
(a): ActualsSetInput => ({
|
||||
id: a.id,
|
||||
recordedOn: a.recordedOn.toISOString().slice(0, 10),
|
||||
year: a.year,
|
||||
comment: a.comment,
|
||||
cash: a.cash,
|
||||
values: a.values as Record<string, { value?: number; mortgage?: number }>,
|
||||
})
|
||||
),
|
||||
origins: plan.scenarios.flatMap((s) => s.elements.map((e) => ({ id: e.id, sourceElementId: e.sourceElementId }))),
|
||||
analyses: analyses.map((a) => ({ name: a.name, type: a.type, result: a.result })),
|
||||
});
|
||||
|
||||
// Rendern und Ablegen sind die beiden Schritte, die zur Laufzeit scheitern können
|
||||
// (Schriftdaten, Grösse des Datensatzes). Ein nacktes 500 wäre hier nicht diagnostizierbar,
|
||||
// deshalb wird die Ursache protokolliert UND zurückgegeben -- es ist ein Ein-Personen-
|
||||
// Werkzeug, und der Nutzer ist der Einzige, der den Fehler melden kann.
|
||||
let pdf: Buffer;
|
||||
// Der GESAMTE weitere Ablauf liegt in einem try/catch. Ein unbehandelter Fehler ergäbe ein
|
||||
// nacktes 500 ohne JSON-Körper -- der Client könnte dann nur «Fehler 500» melden, und die
|
||||
// Ursache bliebe im Dunkeln.
|
||||
try {
|
||||
pdf = await renderReportPdf(model);
|
||||
} catch (err) {
|
||||
console.error("[reports] PDF-Erzeugung fehlgeschlagen", err);
|
||||
return NextResponse.json(
|
||||
{ error: `PDF-Erzeugung fehlgeschlagen: ${err instanceof Error ? err.message : String(err)}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
const plan = await prisma.plan.findFirst({
|
||||
where: { id: planId, userId },
|
||||
include: {
|
||||
persons: { orderBy: { role: "asc" } },
|
||||
scenarios: { orderBy: [{ isBase: "desc" }, { createdAt: "asc" }], select: { id: true, name: true, isBase: true } },
|
||||
actuals: { orderBy: [{ year: "asc" }, { recordedOn: "asc" }] },
|
||||
},
|
||||
});
|
||||
if (!plan) return NextResponse.json({ error: "Plan nicht gefunden." }, { status: 404 });
|
||||
|
||||
// Szenarien einzeln nachladen statt über eine verschachtelte Abfrage: Plan -> Szenarien ->
|
||||
// Plan -> Personen wäre ein Ringbezug, und ein paar Abfragen mehr sind hier belanglos.
|
||||
const wanted = plan.scenarios
|
||||
.filter((s) => cfg.scenarioIds.includes(s.id))
|
||||
.sort((a, b) => (a.isBase === b.isBase ? 0 : a.isBase ? -1 : 1));
|
||||
if (wanted.length === 0) {
|
||||
return NextResponse.json({ error: "Kein gültiges Szenario ausgewählt." }, { status: 400 });
|
||||
}
|
||||
const chosen = (
|
||||
await Promise.all(wanted.map((s) => prisma.scenario.findUnique({ where: { id: s.id }, include: planInclude })))
|
||||
).filter((s): s is NonNullable<typeof s> => !!s);
|
||||
if (chosen.length === 0) {
|
||||
return NextResponse.json({ error: "Szenarien konnten nicht geladen werden." }, { status: 400 });
|
||||
}
|
||||
|
||||
// Element-Herkunft aller Szenarien -- Grundlage der Ist-Zuordnung.
|
||||
const allElements = await prisma.financialElement.findMany({
|
||||
where: { scenario: { planId } },
|
||||
select: { id: true, sourceElementId: true },
|
||||
});
|
||||
|
||||
const analyses =
|
||||
cfg.analysisIds.length === 0
|
||||
? []
|
||||
: await prisma.savedAnalysis.findMany({
|
||||
where: { id: { in: cfg.analysisIds }, planId },
|
||||
orderBy: { createdAt: "asc" },
|
||||
select: { name: true, type: true, result: true },
|
||||
});
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: userId }, select: { username: true } });
|
||||
|
||||
const config: ReportConfig = {
|
||||
title: cfg.title,
|
||||
metric: cfg.metric,
|
||||
source: cfg.source,
|
||||
scenarioIds: chosen.map((s) => s.id),
|
||||
analysisIds: cfg.analysisIds,
|
||||
comment: cfg.comment ?? null,
|
||||
};
|
||||
|
||||
const model = buildReport({
|
||||
planName: plan.name,
|
||||
author: user?.username ?? "unbekannt",
|
||||
createdAt: new Date(),
|
||||
config,
|
||||
scenarios: chosen.map((s) => ({ name: s.name, isBase: s.isBase, plan: toPlanInput(s) })),
|
||||
household: {
|
||||
householdType: plan.householdType,
|
||||
startYear: plan.startYear,
|
||||
persons: plan.persons.map((p) => ({ name: p.name, age: p.age, role: p.role })),
|
||||
},
|
||||
actuals: plan.actuals.map(
|
||||
(a): ActualsSetInput => ({
|
||||
id: a.id,
|
||||
recordedOn: a.recordedOn.toISOString().slice(0, 10),
|
||||
year: a.year,
|
||||
comment: a.comment,
|
||||
cash: a.cash,
|
||||
values: a.values as Record<string, { value?: number; mortgage?: number }>,
|
||||
})
|
||||
),
|
||||
origins: allElements,
|
||||
analyses: analyses.map((a) => ({ name: a.name, type: a.type, result: a.result })),
|
||||
});
|
||||
|
||||
const pdf = await renderReportPdf(model);
|
||||
|
||||
try {
|
||||
const created = await prisma.report.create({
|
||||
data: {
|
||||
planId,
|
||||
@@ -161,10 +163,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
});
|
||||
return NextResponse.json({ report: { id: created.id, bytes: pdf.length } }, { status: 201 });
|
||||
} catch (err) {
|
||||
console.error("[reports] Speichern fehlgeschlagen", err);
|
||||
return NextResponse.json(
|
||||
{ error: `Bericht konnte nicht gespeichert werden: ${err instanceof Error ? err.message : String(err)}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
// Jede Ursache landet hier -- Datenbank, Modellaufbau, Schriftdaten, Speichern.
|
||||
console.error("[reports] Bericht fehlgeschlagen", err);
|
||||
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
||||
return NextResponse.json({ error: `Bericht fehlgeschlagen -- ${message}` }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user