fix: Bug 1+2+3 — SQL Cross-Join, PATCH 404 vs 400, API-Test-Skip

- getCampaignRecipients: EXISTS-Subquery statt Cross-Join verhindert Mehrfachversand
- updateCampaign: SELECT vor UPDATE unterscheidet 'nicht gefunden' (404) von 'nicht im Draft' (400)
- campaigns-api.test.ts: describe.skip entfernt, Mocks für DB-Abhängigkeiten ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 17:34:18 +00:00
parent ca0c65352e
commit e5db71ead1
4 changed files with 114 additions and 33 deletions

View File

@@ -1,7 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
import { getCampaign, updateCampaignStatus } from '../../../../../server/db/campaigns'
import { getCampaign, updateCampaignStatus, getCampaignRecipients } from '../../../../../server/db/campaigns'
import { emailSendQueue } from '../../../../../queues/email-send.queue'
import { withTenant } from '../../../../../server/db/tenant'
import { hashEmail } from '../../../../../lib/crypto'
import { checkSuppression } from '../../../../../server/suppression/check'
import { getTenantId } from '../../../../../lib/tenant-header'
@@ -25,22 +24,13 @@ export async function POST(req: NextRequest, { params }: { params: { id: string
return NextResponse.json({ error: statusResult.error.message }, { status: 500 })
}
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) {
const recipientsResult = await getCampaignRecipients(tenantId, params.id)
if (!recipientsResult.ok) {
// 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 })
}
const recipients = recipientsResult.data
// Suppression-Check PFLICHT — kein Opt-out-Empfänger darf in die Queue
const unsuppressedRecipients = await Promise.all(