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>
98 lines
2.8 KiB
TypeScript
98 lines
2.8 KiB
TypeScript
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 }
|
|
}
|