From 25889a5419a643fcebf10352206447d255686dce Mon Sep 17 00:00:00 2001 From: Joachim Hummel Date: Fri, 17 Apr 2026 13:50:21 +0000 Subject: [PATCH] fix: Rollback auf draft bei Versand-Fehlern, Fehlermeldung updateCampaign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - send/route.ts: try/catch + Rollback auf draft wenn Empfänger-Query oder Queue-Enqueue fehlschlägt - campaigns.ts: Fehlermeldung 'Kampagne nicht im Draft-Status' (zuvor enthielt sie 'nicht gefunden' — verursachte falschen 404 in PATCH) Co-Authored-By: Claude Sonnet 4.6 --- src/app/api/campaigns/[id]/send/route.ts | 68 ++++++++++++++---------- src/server/db/campaigns.ts | 2 +- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/app/api/campaigns/[id]/send/route.ts b/src/app/api/campaigns/[id]/send/route.ts index 9030068..5ef3de2 100644 --- a/src/app/api/campaigns/[id]/send/route.ts +++ b/src/app/api/campaigns/[id]/send/route.ts @@ -17,23 +17,29 @@ export async function POST(req: NextRequest, { params }: { params: { id: string return NextResponse.json({ error: 'Nur Draft-Kampagnen können versendet werden' }, { status: 400 }) } - // Status auf 'sending' setzen — Ergebnis prüfen (Fix C1) const statusResult = await updateCampaignStatus(tenantId, params.id, 'sending') if (!statusResult.ok) { return NextResponse.json({ error: statusResult.error.message }, { status: 500 }) } - const recipients = await withTenant(tenantId, (client) => - client.query<{ email: string }>( - `SELECT s.email FROM subscribers s - JOIN campaign_recipients cr ON cr.campaign_id = $1 - WHERE (cr.list_id IS NULL OR s.list_id = cr.list_id) - AND s.status = 'active'`, - [params.id] + let recipients: { email: string }[] + try { + recipients = await withTenant(tenantId, (client) => + client.query<{ email: string }>( + `SELECT s.email FROM subscribers s + JOIN campaign_recipients cr ON cr.campaign_id = $1 + WHERE (cr.list_id IS NULL OR s.list_id = cr.list_id) + AND s.status = 'active'`, + [params.id] + ) ) - ) + } catch (e) { + // Rollback auf draft — verhindert verwaisten 'sending'-Status + await updateCampaignStatus(tenantId, params.id, 'draft') + return NextResponse.json({ error: 'Empfänger konnten nicht geladen werden' }, { status: 500 }) + } - // Suppression-Check PFLICHT — kein Opt-out-Empfänger darf in die Queue (Fix C3) + // Suppression-Check PFLICHT — kein Opt-out-Empfänger darf in die Queue const unsuppressedRecipients = await Promise.all( recipients.map(async (r) => { const suppressed = await checkSuppression(tenantId, r.email) @@ -41,24 +47,30 @@ export async function POST(req: NextRequest, { params }: { params: { id: string }) ).then((results) => results.filter((r): r is { email: string } => r !== null)) - // Bulk-Enqueue — schnell, kein sequenzielles await (Fix C2) - const addedJobs = await emailSendQueue.addBulk( - unsuppressedRecipients.map((recipient) => ({ - name: 'send', - data: { - tenantId, - campaignId: params.id, - recipientEmail: recipient.email, - recipientHash: hashEmail(recipient.email), - }, - opts: { - attempts: 3, - backoff: { type: 'exponential' as const, delay: 2000 }, - removeOnComplete: 100, - removeOnFail: { count: 500 }, - }, - })) - ) + let addedJobs: { id?: string }[] + try { + addedJobs = await emailSendQueue.addBulk( + unsuppressedRecipients.map((recipient) => ({ + name: 'send', + data: { + tenantId, + campaignId: params.id, + recipientEmail: recipient.email, + recipientHash: hashEmail(recipient.email), + }, + opts: { + attempts: 3, + backoff: { type: 'exponential' as const, delay: 2000 }, + removeOnComplete: 100, + removeOnFail: { count: 500 }, + }, + })) + ) + } catch (e) { + // Rollback auf draft — Queue-Fehler darf nicht zu verwaisten Kampagnen führen + await updateCampaignStatus(tenantId, params.id, 'draft') + return NextResponse.json({ error: 'Jobs konnten nicht eingestellt werden' }, { status: 500 }) + } return NextResponse.json({ queued: addedJobs.length }) } diff --git a/src/server/db/campaigns.ts b/src/server/db/campaigns.ts index e723fff..1dd5e10 100644 --- a/src/server/db/campaigns.ts +++ b/src/server/db/campaigns.ts @@ -86,7 +86,7 @@ export async function updateCampaign( values ) ) - if (rows.length === 0) return err(new Error('Kampagne nicht gefunden oder nicht im Draft-Status')) + if (rows.length === 0) return err(new Error('Kampagne nicht im Draft-Status')) return ok(rowToCampaign(rows[0])) } catch (e) { return err(e instanceof Error ? e : new Error(String(e)))