fix: safeInsertEvent verhindert Doppelversand, Union-Typ für eventType, Privacy-Tests ergänzt
This commit is contained in:
@@ -65,10 +65,15 @@ describe('processEmailSendJob', () => {
|
|||||||
expect(clickhouse.insert).toHaveBeenCalledWith(
|
expect(clickhouse.insert).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
values: expect.arrayContaining([
|
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 () => {
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
// Suppression-Check ist PFLICHT — kein Opt-out-Empfänger darf E-Mail erhalten
|
||||||
const suppressed = await checkSuppression(data.tenantId, data.recipientEmail)
|
const suppressed = await checkSuppression(data.tenantId, data.recipientEmail)
|
||||||
if (suppressed) {
|
if (suppressed) {
|
||||||
await insertEvent('suppressed', data)
|
await safeInsertEvent('suppressed', data)
|
||||||
return ok(undefined)
|
return ok(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,26 +31,31 @@ export async function processEmailSendJob(data: EmailSendJobData): Promise<Resul
|
|||||||
|
|
||||||
if (!sendResult.ok) return err(sendResult.error)
|
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)
|
return ok(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function insertEvent(eventType: string, data: EmailSendJobData): Promise<void> {
|
async function safeInsertEvent(
|
||||||
await clickhouse.insert({
|
eventType: 'sent' | 'suppressed',
|
||||||
table: 'email_events',
|
data: EmailSendJobData
|
||||||
values: [
|
): Promise<void> {
|
||||||
{
|
try {
|
||||||
|
await clickhouse.insert({
|
||||||
|
table: 'email_events',
|
||||||
|
values: [{
|
||||||
event_type: eventType,
|
event_type: eventType,
|
||||||
tenant_id: data.tenantId,
|
tenant_id: data.tenantId,
|
||||||
campaign_id: data.campaignId,
|
campaign_id: data.campaignId,
|
||||||
// Datenschutz: nur Hash wird gespeichert — keine Klartext-E-Mail-Adresse in ClickHouse
|
|
||||||
recipient_hash: data.recipientHash,
|
recipient_hash: data.recipientHash,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
metadata: {},
|
metadata: {},
|
||||||
},
|
}],
|
||||||
],
|
format: 'JSONEachRow',
|
||||||
format: 'JSONEachRow',
|
})
|
||||||
})
|
} catch (e) {
|
||||||
|
console.error(JSON.stringify({ level: 'warn', msg: 'ClickHouse insert fehlgeschlagen', error: String(e) }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const connection = {
|
const connection = {
|
||||||
|
|||||||
Reference in New Issue
Block a user