fix: safeInsertEvent verhindert Doppelversand, Union-Typ für eventType, Privacy-Tests ergänzt

This commit is contained in:
2026-04-17 12:13:31 +00:00
parent 8bf143735e
commit e49552236d
2 changed files with 34 additions and 13 deletions

View File

@@ -65,10 +65,15 @@ describe('processEmailSendJob', () => {
expect(clickhouse.insert).toHaveBeenCalledWith(
expect.objectContaining({
values: expect.arrayContaining([
expect.objectContaining({ event_type: 'suppressed' }),
expect.objectContaining({
event_type: 'suppressed',
recipient_hash: 'abc123hash',
}),
]),
})
)
const insertCall = vi.mocked(clickhouse.insert).mock.calls[0][0]
expect(JSON.stringify(insertCall.values)).not.toContain('empfaenger@example.com')
})
it('gibt err zurück wenn Kampagne nicht gefunden', async () => {
@@ -117,4 +122,15 @@ describe('processEmailSendJob', () => {
})
)
})
it('ClickHouse-Fehler nach erfolgreichem SMTP führt nicht zu err (verhindert Doppelversand)', async () => {
vi.mocked(getCampaign).mockResolvedValue({ ok: true, data: mockCampaign })
vi.mocked(checkSuppression).mockResolvedValue(false)
vi.mocked(sendEmail).mockResolvedValue({ ok: true, data: undefined })
vi.mocked(clickhouse.insert).mockRejectedValueOnce(new Error('ClickHouse down'))
const result = await processEmailSendJob(jobData)
expect(result.ok).toBe(true)
})
})

View File

@@ -14,7 +14,7 @@ export async function processEmailSendJob(data: EmailSendJobData): Promise<Resul
// Suppression-Check ist PFLICHT — kein Opt-out-Empfänger darf E-Mail erhalten
const suppressed = await checkSuppression(data.tenantId, data.recipientEmail)
if (suppressed) {
await insertEvent('suppressed', data)
await safeInsertEvent('suppressed', data)
return ok(undefined)
}
@@ -31,26 +31,31 @@ export async function processEmailSendJob(data: EmailSendJobData): Promise<Resul
if (!sendResult.ok) return err(sendResult.error)
await insertEvent('sent', data)
// Analytics-Fehler nicht an BullMQ weitergeben — verhindert Doppelversand bei Retry
await safeInsertEvent('sent', data)
return ok(undefined)
}
async function insertEvent(eventType: string, data: EmailSendJobData): Promise<void> {
async function safeInsertEvent(
eventType: 'sent' | 'suppressed',
data: EmailSendJobData
): Promise<void> {
try {
await clickhouse.insert({
table: 'email_events',
values: [
{
values: [{
event_type: eventType,
tenant_id: data.tenantId,
campaign_id: data.campaignId,
// Datenschutz: nur Hash wird gespeichert — keine Klartext-E-Mail-Adresse in ClickHouse
recipient_hash: data.recipientHash,
timestamp: new Date().toISOString(),
metadata: {},
},
],
}],
format: 'JSONEachRow',
})
} catch (e) {
console.error(JSON.stringify({ level: 'warn', msg: 'ClickHouse insert fehlgeschlagen', error: String(e) }))
}
}
const connection = {