import { supabase } from "@/integrations/supabase/client"; import { invokeWithTracking } from "@/lib/edgeFunctionTracking"; import { logger } from "@/lib/logger"; import { 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) { logger.error('Failed to check Novu status', { action: 'check_novu_status', error: error instanceof Error ? error.message : String(error) }); 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) { logger.warn('Novu not configured, skipping subscriber update', { action: 'update_novu_subscriber', userId: validated.subscriberId }); return { success: false, error: 'Novu not configured' }; } const { data, error, requestId } = await invokeWithTracking( 'update-novu-subscriber', validated ); if (error) { logger.error('Edge function error updating Novu subscriber', { action: 'update_novu_subscriber', userId: validated.subscriberId, requestId, error: error.message }); throw new AppError( 'Failed to update notification subscriber', 'NOTIFICATION_ERROR', error.message ); } logger.info('Novu subscriber updated successfully', { action: 'update_novu_subscriber', userId: validated.subscriberId, requestId }); return { success: true }; } catch (error: unknown) { logger.error('Error in updateSubscriber', { action: 'update_novu_subscriber', userId: subscriberData.subscriberId, error: error instanceof Error ? error.message : String(error) }); 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) { logger.warn('Novu not configured, skipping subscriber creation', { action: 'create_novu_subscriber', userId: validated.subscriberId }); return { success: false, error: 'Novu not configured' }; } const { data, error, requestId } = await invokeWithTracking( 'create-novu-subscriber', validated ); if (error) { logger.error('Edge function error creating Novu subscriber', { action: 'create_novu_subscriber', userId: validated.subscriberId, requestId, error: error.message }); 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) { logger.error('Failed to store subscriber preferences', { action: 'store_subscriber_preferences', userId: validated.subscriberId, error: dbError.message, errorCode: dbError.code }); throw dbError; } logger.info('Novu subscriber created successfully', { action: 'create_novu_subscriber', userId: validated.subscriberId, requestId }); return { success: true }; } catch (error: unknown) { logger.error('Error in createSubscriber', { action: 'create_novu_subscriber', userId: subscriberData.subscriberId, error: error instanceof Error ? error.message : String(error) }); 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) { logger.error('Failed to update Novu preferences', { action: 'update_novu_preferences', userId, requestId, error: novuError.message }); 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) { logger.error('Failed to save notification preferences', { action: 'save_notification_preferences', userId, error: dbError.message, errorCode: dbError.code }); throw dbError; } // Create audit log entry // DOCUMENTED EXCEPTION: profile_audit_log.changes column accepts JSONB // We validate the preferences structure with Zod before this point // Safe because the payload is constructed type-safely earlier in the function await supabase.from('profile_audit_log').insert([{ user_id: userId, changed_by: userId, action: 'notification_preferences_updated', changes: { previous: previousPrefs || null, updated: validated, timestamp: new Date().toISOString() } }]); logger.info('Notification preferences updated', { action: 'update_notification_preferences', userId }); return { success: true }; } catch (error: unknown) { logger.error('Error updating notification preferences', { action: 'update_notification_preferences', userId, error: error instanceof Error ? error.message : String(error) }); 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') { logger.error('Failed to fetch notification preferences', { action: 'fetch_notification_preferences', userId, error: error.message, errorCode: error.code }); throw error; } if (!data) { logger.info('No preferences found, returning defaults', { action: 'fetch_notification_preferences', userId }); 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) { logger.error('Error fetching notification preferences', { action: 'fetch_notification_preferences', userId, error: error instanceof Error ? error.message : String(error) }); 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) { logger.error('Failed to fetch notification templates', { action: 'fetch_notification_templates', error: error.message, errorCode: error.code }); throw error; } return data || []; } catch (error: unknown) { logger.error('Error fetching notification templates', { action: 'fetch_notification_templates', error: error instanceof Error ? error.message : String(error) }); return []; } } /** * Trigger a notification workflow */ async trigger(payload: NotificationPayload): Promise<{ success: boolean; error?: string }> { try { const novuEnabled = await this.isNovuEnabled(); if (!novuEnabled) { logger.warn('Novu not configured, skipping notification', { action: 'trigger_notification', workflowId: payload.workflowId, subscriberId: payload.subscriberId }); return { success: false, error: 'Novu not configured' }; } const { data, error, requestId } = await invokeWithTracking( 'trigger-notification', payload ); if (error) { logger.error('Failed to trigger notification', { action: 'trigger_notification', workflowId: payload.workflowId, subscriberId: payload.subscriberId, requestId, error: error.message }); throw error; } logger.info('Notification triggered successfully', { action: 'trigger_notification', workflowId: payload.workflowId, subscriberId: payload.subscriberId, transactionId: data?.transactionId, requestId }); return { success: true }; } catch (error: unknown) { logger.error('Error triggering notification', { action: 'trigger_notification', workflowId: payload.workflowId, subscriberId: payload.subscriberId, error: error instanceof Error ? error.message : String(error) }); 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) { logger.error('Failed to notify moderators', { action: 'notify_moderators', submissionId: payload.submission_id, requestId, error: error.message }); throw error; } logger.info('Moderators notified successfully', { action: 'notify_moderators', submissionId: payload.submission_id, requestId }); } catch (error: unknown) { logger.error('Error notifying moderators', { action: 'notify_moderators', submissionId: payload.submission_id, error: error instanceof Error ? error.message : String(error) }); } } /** * Check if notifications are enabled */ isEnabled(): boolean { return true; // Always return true, actual check happens in isNovuEnabled() } } export const notificationService = new NotificationService();