import { supabase } from "@/lib/supabaseClient"; import { invokeWithTracking } from "@/lib/edgeFunctionTracking"; import { handleNonCriticalError, AppError } from "@/lib/errorHandler"; import { z } from "zod"; import type { NotificationPayload, SubscriberData, NotificationPreferences, NotificationTemplate } from "@/types/notifications"; import { notificationPreferencesSchema, subscriberDataSchema, DEFAULT_NOTIFICATION_PREFERENCES } from "@/lib/notificationValidation"; class NotificationService { /** * Check if Novu is enabled by checking admin settings */ async isNovuEnabled(): Promise { try { const { data } = await supabase .from('admin_settings') .select('setting_value') .eq('setting_key', 'novu.application_identifier') .maybeSingle(); return !!data?.setting_value; } catch (error: unknown) { handleNonCriticalError(error, { action: 'Check Novu Status', metadata: { returnedFalse: true } }); return false; } } /** * Update an existing Novu subscriber's profile information */ async updateSubscriber(subscriberData: SubscriberData): Promise<{ success: boolean; error?: string }> { try { // Validate input const validated = subscriberDataSchema.parse(subscriberData); const novuEnabled = await this.isNovuEnabled(); if (!novuEnabled) { return { success: false, error: 'Novu not configured' }; } const { data, error, requestId } = await invokeWithTracking( 'update-novu-subscriber', validated ); if (error) { handleNonCriticalError(error, { action: 'Update Novu Subscriber (Edge Function)', userId: validated.subscriberId, metadata: { requestId } }); throw new AppError( 'Failed to update notification subscriber', 'NOTIFICATION_ERROR', error.message ); } return { success: true }; } catch (error: unknown) { handleNonCriticalError(error, { action: 'Update Novu Subscriber', userId: subscriberData.subscriberId }); return { success: false, error: error instanceof AppError ? error.message : 'Failed to update subscriber' }; } } /** * Create or update a Novu subscriber */ async createSubscriber(subscriberData: SubscriberData): Promise<{ success: boolean; error?: string }> { try { // Validate input const validated = subscriberDataSchema.parse(subscriberData); const novuEnabled = await this.isNovuEnabled(); if (!novuEnabled) { return { success: false, error: 'Novu not configured' }; } const { data, error, requestId } = await invokeWithTracking( 'create-novu-subscriber', validated ); if (error) { handleNonCriticalError(error, { action: 'Create Novu Subscriber (Edge Function)', userId: validated.subscriberId, metadata: { requestId } }); throw new AppError( 'Failed to create notification subscriber', 'NOTIFICATION_ERROR', error.message ); } if (!data?.subscriberId) { throw new AppError( 'Invalid response from notification service', 'NOTIFICATION_ERROR' ); } // Store subscriber ID in database const { error: dbError } = await supabase .from('user_notification_preferences') .upsert({ user_id: validated.subscriberId, novu_subscriber_id: data.subscriberId, }); if (dbError) { handleNonCriticalError(dbError, { action: 'Store Subscriber Preferences', userId: validated.subscriberId }); throw dbError; } return { success: true }; } catch (error: unknown) { handleNonCriticalError(error, { action: 'Create Novu Subscriber', userId: subscriberData.subscriberId }); return { success: false, error: error instanceof AppError ? error.message : 'Failed to create subscriber' }; } } /** * Update notification preferences with validation and audit logging */ async updatePreferences( userId: string, preferences: NotificationPreferences ): Promise<{ success: boolean; error?: string }> { try { // Validate preferences const validated = notificationPreferencesSchema.parse(preferences); // Get previous preferences for audit log const { data: previousPrefs } = await supabase .from('user_notification_preferences') .select('channel_preferences, workflow_preferences, frequency_settings') .eq('user_id', userId) .maybeSingle(); const novuEnabled = await this.isNovuEnabled(); // Update Novu preferences if enabled if (novuEnabled) { const { error: novuError, requestId } = await invokeWithTracking( 'update-novu-preferences', { userId, preferences: validated, } ); if (novuError) { handleNonCriticalError(novuError, { action: 'Update Novu Preferences', userId, metadata: { requestId } }); throw novuError; } } // Update local database const { error: dbError } = await supabase .from('user_notification_preferences') .upsert({ user_id: userId, channel_preferences: validated.channelPreferences, workflow_preferences: validated.workflowPreferences, frequency_settings: validated.frequencySettings, }); if (dbError) { handleNonCriticalError(dbError, { action: 'Save Notification Preferences', userId }); throw dbError; } // Create audit log entry using relational tables const { data: auditLog, error: auditError } = await supabase .from('profile_audit_log') .insert([{ user_id: userId, changed_by: userId, action: 'notification_preferences_updated', changes: {}, // Empty placeholder - actual changes stored in profile_change_fields table }]) .select('id') .single(); if (!auditError && auditLog) { // Write changes to relational profile_change_fields table const { writeProfileChangeFields } = await import('./auditHelpers'); await writeProfileChangeFields(auditLog.id, { email_notifications: { old_value: previousPrefs?.channel_preferences, new_value: validated.channelPreferences, }, workflow_preferences: { old_value: previousPrefs?.workflow_preferences, new_value: validated.workflowPreferences, }, frequency_settings: { old_value: previousPrefs?.frequency_settings, new_value: validated.frequencySettings, }, }); } return { success: true }; } catch (error: unknown) { handleNonCriticalError(error, { action: 'Update Notification Preferences', userId }); if (error instanceof z.ZodError) { return { success: false, error: `Invalid preferences: ${error.issues.map(i => i.message).join(', ')}` }; } return { success: false, error: 'Failed to update notification preferences' }; } } /** * Get user's notification preferences with proper typing */ async getPreferences(userId: string): Promise { try { const { data, error } = await supabase .from('user_notification_preferences') .select('channel_preferences, workflow_preferences, frequency_settings') .eq('user_id', userId) .maybeSingle(); if (error && error.code !== 'PGRST116') { handleNonCriticalError(error, { action: 'Fetch Notification Preferences', userId }); throw error; } if (!data) { return DEFAULT_NOTIFICATION_PREFERENCES; } // Validate the data from database return notificationPreferencesSchema.parse({ channelPreferences: data.channel_preferences, workflowPreferences: data.workflow_preferences, frequencySettings: data.frequency_settings }); } catch (error: unknown) { handleNonCriticalError(error, { action: 'Get Notification Preferences', userId }); return null; } } /** * Get notification templates */ async getTemplates(): Promise { try { const { data, error } = await supabase .from('notification_templates') .select('*') .eq('is_active', true) .order('category', { ascending: true }); if (error) { handleNonCriticalError(error, { action: 'Fetch Notification Templates' }); throw error; } return (data || []).map(t => ({ ...t, is_active: t.is_active ?? true, description: t.description || null, novu_workflow_id: t.novu_workflow_id || null, })); } catch (error: unknown) { handleNonCriticalError(error, { action: 'Get Notification Templates' }); return []; } } /** * Trigger a notification workflow */ async trigger(payload: NotificationPayload): Promise<{ success: boolean; error?: string }> { try { const novuEnabled = await this.isNovuEnabled(); if (!novuEnabled) { return { success: false, error: 'Novu not configured' }; } const { data, error, requestId } = await invokeWithTracking( 'trigger-notification', payload ); if (error) { handleNonCriticalError(error, { action: 'Trigger Notification', metadata: { workflowId: payload.workflowId, subscriberId: payload.subscriberId, requestId } }); throw error; } return { success: true }; } catch (error: unknown) { handleNonCriticalError(error, { action: 'Trigger Notification', metadata: { workflowId: payload.workflowId, subscriberId: payload.subscriberId } }); return { success: false, error: 'Failed to trigger notification' }; } } /** * Notify moderators (legacy method for backward compatibility) */ async notifyModerators(payload: { submission_id: string; submission_type: string; submitter_name: string; action: string; }): Promise { try { const { error, requestId } = await invokeWithTracking( 'notify-moderators-submission', payload ); if (error) { handleNonCriticalError(error, { action: 'Notify Moderators (Submission)', metadata: { submissionId: payload.submission_id, requestId } }); throw error; } } catch (error: unknown) { handleNonCriticalError(error, { action: 'Notify Moderators (Submission)', metadata: { submissionId: payload.submission_id } }); } } /** * Trigger a system announcement to all users via the "users" topic * Requires admin or superuser role */ async sendSystemAnnouncement(payload: { title: string; message: string; severity: 'info' | 'warning' | 'critical'; actionUrl?: string; }): Promise<{ success: boolean; error?: string; announcementId?: string }> { try { const novuEnabled = await this.isNovuEnabled(); if (!novuEnabled) { return { success: false, error: 'Novu not configured' }; } const { data, error, requestId } = await invokeWithTracking( 'notify-system-announcement', payload ); if (error) { handleNonCriticalError(error, { action: 'Send System Announcement', metadata: { title: payload.title, requestId } }); throw error; } return { success: true, announcementId: data?.announcementId }; } catch (error: unknown) { handleNonCriticalError(error, { action: 'Send System Announcement', metadata: { title: payload.title } }); return { success: false, error: error instanceof Error ? error.message : 'Failed to send system announcement' }; } } /** * Notify moderators about a new report via the "moderation-reports" topic */ async notifyModeratorsReport(payload: { reportId: string; reportType: string; reportedEntityType: string; reportedEntityId: string; reporterName: string; reason: string; entityPreview: string; reportedAt: string; }): Promise<{ success: boolean; error?: string }> { try { const novuEnabled = await this.isNovuEnabled(); if (!novuEnabled) { return { success: false, error: 'Novu not configured' }; } const { data, error, requestId } = await invokeWithTracking( 'notify-moderators-report', payload ); if (error) { handleNonCriticalError(error, { action: 'Notify Moderators (Report)', metadata: { reportId: payload.reportId, requestId } }); throw error; } return { success: true }; } catch (error: unknown) { handleNonCriticalError(error, { action: 'Notify Moderators (Report)', metadata: { reportId: payload.reportId } }); return { success: false, error: 'Failed to notify moderators about report' }; } } /** * Check if notifications are enabled */ isEnabled(): boolean { return true; // Always return true, actual check happens in isNovuEnabled() } } export const notificationService = new NotificationService();