feat: extend notification system with batch sender, bulk dialog, and logging
Add NotificationLog schema extensions (nullable userId, email, roundId, projectId, batchId fields), batch notification sender service, and bulk notification dialog UI. Include utility scripts for debugging and seeding. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
97
src/server/services/notification-sender.ts
Normal file
97
src/server/services/notification-sender.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { sendStyledNotificationEmail, emailDelay } from '@/lib/email'
|
||||
import type { NotificationEmailContext } from '@/lib/email'
|
||||
|
||||
export type NotificationItem = {
|
||||
email: string
|
||||
name: string
|
||||
type: string // ADVANCEMENT_NOTIFICATION, REJECTION_NOTIFICATION, etc.
|
||||
context: NotificationEmailContext
|
||||
projectId?: string
|
||||
userId?: string
|
||||
roundId?: string
|
||||
}
|
||||
|
||||
export type BatchResult = {
|
||||
sent: number
|
||||
failed: number
|
||||
batchId: string
|
||||
errors: Array<{ email: string; error: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notifications in batches with throttling and per-email logging.
|
||||
* Each email is logged to NotificationLog with SENT or FAILED status.
|
||||
*/
|
||||
export async function sendBatchNotifications(
|
||||
items: NotificationItem[],
|
||||
options?: { batchSize?: number; batchDelayMs?: number }
|
||||
): Promise<BatchResult> {
|
||||
const batchId = randomUUID()
|
||||
const batchSize = options?.batchSize ?? 10
|
||||
const batchDelayMs = options?.batchDelayMs ?? 500
|
||||
|
||||
let sent = 0
|
||||
let failed = 0
|
||||
const errors: Array<{ email: string; error: string }> = []
|
||||
|
||||
for (let i = 0; i < items.length; i += batchSize) {
|
||||
const chunk = items.slice(i, i + batchSize)
|
||||
|
||||
for (const item of chunk) {
|
||||
try {
|
||||
await sendStyledNotificationEmail(
|
||||
item.email,
|
||||
item.name,
|
||||
item.type,
|
||||
item.context,
|
||||
)
|
||||
sent++
|
||||
|
||||
// Log success (fire-and-forget)
|
||||
prisma.notificationLog.create({
|
||||
data: {
|
||||
userId: item.userId || null,
|
||||
channel: 'EMAIL',
|
||||
type: item.type,
|
||||
status: 'SENT',
|
||||
email: item.email,
|
||||
roundId: item.roundId || null,
|
||||
projectId: item.projectId || null,
|
||||
batchId,
|
||||
},
|
||||
}).catch((err) => console.error('[notification-sender] Log write failed:', err))
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err)
|
||||
failed++
|
||||
errors.push({ email: item.email, error: errorMsg })
|
||||
console.error(`[notification-sender] Failed for ${item.email}:`, err)
|
||||
|
||||
// Log failure (fire-and-forget)
|
||||
prisma.notificationLog.create({
|
||||
data: {
|
||||
userId: item.userId || null,
|
||||
channel: 'EMAIL',
|
||||
type: item.type,
|
||||
status: 'FAILED',
|
||||
email: item.email,
|
||||
roundId: item.roundId || null,
|
||||
projectId: item.projectId || null,
|
||||
batchId,
|
||||
errorMsg,
|
||||
},
|
||||
}).catch((logErr) => console.error('[notification-sender] Log write failed:', logErr))
|
||||
}
|
||||
|
||||
await emailDelay()
|
||||
}
|
||||
|
||||
// Delay between chunks to avoid overwhelming SMTP
|
||||
if (i + batchSize < items.length) {
|
||||
await new Promise((resolve) => setTimeout(resolve, batchDelayMs))
|
||||
}
|
||||
}
|
||||
|
||||
return { sent, failed, batchId, errors }
|
||||
}
|
||||
Reference in New Issue
Block a user