import { supabase } from "@/integrations/supabase/client"; export interface NotificationPayload { workflowId: string; subscriberId: string; payload: Record; overrides?: Record; } export interface SubscriberData { subscriberId: string; email?: string; firstName?: string; lastName?: string; phone?: string; avatar?: string; data?: Record; } export interface NotificationPreferences { channelPreferences: { in_app: boolean; email: boolean; push: boolean; sms: boolean; }; workflowPreferences: Record; frequencySettings: { digest: 'realtime' | 'hourly' | 'daily' | 'weekly'; max_per_hour: number; }; } class NotificationService { private readonly isNovuEnabled: boolean; constructor() { this.isNovuEnabled = !!import.meta.env.VITE_NOVU_APPLICATION_IDENTIFIER; } private extractErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') { return error.message; } return 'An unexpected error occurred'; } /** * Create or update a Novu subscriber */ async createSubscriber(subscriberData: SubscriberData): Promise<{ success: boolean; error?: string }> { if (!this.isNovuEnabled) { console.warn('Novu is not configured. Skipping subscriber creation.'); return { success: false, error: 'Novu not configured' }; } try { const { data, error } = await supabase.functions.invoke('create-novu-subscriber', { body: subscriberData, }); if (error) { console.error('Edge function error creating Novu subscriber:', error); throw error; } if (!data || !data.subscriberId) { const errorMsg = 'Invalid response from create-novu-subscriber function'; console.error(errorMsg, { data }); return { success: false, error: errorMsg }; } const { error: dbError } = await supabase .from('user_notification_preferences') .upsert({ user_id: subscriberData.subscriberId, novu_subscriber_id: data.subscriberId, }); if (dbError) { console.error('Database error storing subscriber preferences:', dbError); throw dbError; } console.log('Novu subscriber created successfully:', data.subscriberId); return { success: true }; } catch (error: unknown) { console.error('Error creating Novu subscriber:', error); return { success: false, error: this.extractErrorMessage(error) }; } } /** * Update an existing Novu subscriber's profile information */ async updateSubscriber(subscriberData: SubscriberData): Promise<{ success: boolean; error?: string }> { if (!this.isNovuEnabled) { console.warn('Novu is not configured. Skipping subscriber update.'); return { success: false, error: 'Novu not configured' }; } try { const { data, error } = await supabase.functions.invoke('update-novu-subscriber', { body: subscriberData, }); if (error) { console.error('Edge function error updating Novu subscriber:', error); throw error; } console.log('Novu subscriber updated successfully:', subscriberData.subscriberId); return { success: true }; } catch (error: unknown) { console.error('Error updating Novu subscriber:', error); return { success: false, error: this.extractErrorMessage(error) }; } } /** * Update subscriber preferences in Novu */ async updatePreferences( userId: string, preferences: NotificationPreferences ): Promise<{ success: boolean; error?: string }> { if (!this.isNovuEnabled) { try { const { error } = await supabase .from('user_notification_preferences') .upsert({ user_id: userId, channel_preferences: preferences.channelPreferences, workflow_preferences: preferences.workflowPreferences, frequency_settings: preferences.frequencySettings, }); if (error) { console.error('Database error saving preferences (Novu disabled):', error); throw error; } console.log('Preferences saved to local database:', userId); return { success: true }; } catch (error: unknown) { console.error('Error saving preferences to local database:', error); return { success: false, error: this.extractErrorMessage(error) }; } } try { const { error } = await supabase.functions.invoke('update-novu-preferences', { body: { userId, preferences, }, }); if (error) { console.error('Edge function error updating Novu preferences:', error); throw error; } const { error: dbError } = await supabase .from('user_notification_preferences') .upsert({ user_id: userId, channel_preferences: preferences.channelPreferences, workflow_preferences: preferences.workflowPreferences, frequency_settings: preferences.frequencySettings, }); if (dbError) { console.error('Database error saving preferences locally:', dbError); throw dbError; } console.log('Preferences updated successfully:', userId); return { success: true }; } catch (error: unknown) { console.error('Error updating preferences:', error); return { success: false, error: this.extractErrorMessage(error) }; } } /** * Trigger a notification workflow */ async trigger(payload: NotificationPayload): Promise<{ success: boolean; transactionId?: string; error?: string }> { if (!this.isNovuEnabled) { console.warn('Novu is not configured. Notification not sent.'); return { success: false, error: 'Novu not configured' }; } try { const { data, error } = await supabase.functions.invoke('trigger-notification', { body: payload, }); if (error) { console.error('Edge function error triggering notification:', error); throw error; } if (!data || !data.transactionId) { const errorMsg = 'Invalid response from trigger-notification function'; console.error(errorMsg, { data }); return { success: false, error: errorMsg }; } await this.logNotification({ userId: payload.subscriberId, workflowId: payload.workflowId, transactionId: data.transactionId, payload: payload.payload, }); console.log('Notification triggered successfully:', data.transactionId); return { success: true, transactionId: data.transactionId }; } catch (error: unknown) { console.error('Error triggering notification:', error); return { success: false, error: this.extractErrorMessage(error) }; } } /** * Get user's notification preferences */ 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) .single(); if (error && error.code !== 'PGRST116') { console.error('Database error fetching preferences:', error); throw error; } if (!data) { console.log('No preferences found for user, returning defaults:', userId); return { channelPreferences: { in_app: true, email: true, push: false, sms: false, }, workflowPreferences: {}, frequencySettings: { digest: 'daily', max_per_hour: 10, }, }; } return { channelPreferences: data.channel_preferences as any, workflowPreferences: data.workflow_preferences as any, frequencySettings: data.frequency_settings as any, }; } catch (error: unknown) { console.error('Error fetching notification preferences for user:', userId, 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) { console.error('Database error fetching notification templates:', error); throw error; } return data || []; } catch (error: unknown) { console.error('Error fetching notification templates:', error); return []; } } /** * Log notification in database */ private async logNotification(log: { userId: string; workflowId: string; transactionId: string; payload: Record; }): Promise { try { // Get template ID from workflow ID const { data: template } = await supabase .from('notification_templates') .select('id') .eq('workflow_id', log.workflowId) .single(); const { error: logError } = await supabase.from('notification_logs').insert({ user_id: log.userId, template_id: template?.id, novu_transaction_id: log.transactionId, channel: 'multi', status: 'sent', payload: log.payload, }); if (logError) { console.error('Error inserting notification log:', logError); } } catch (error) { console.error('Error logging notification:', error); } } /** * Notify all moderators about a new submission */ async notifyModerators(payload: { submission_id: string; submission_type: string; submitter_name: string; action: string; }): Promise<{ success: boolean; count: number; error?: string }> { try { const { data, error } = await supabase.functions.invoke('notify-moderators-submission', { body: payload, }); if (error) { console.error('Edge function error notifying moderators:', error); throw error; } console.log('Moderators notified successfully:', data); return { success: true, count: data?.count || 0 }; } catch (error: unknown) { console.error('Error notifying moderators:', error); return { success: false, count: 0, error: this.extractErrorMessage(error) }; } } /** * Check if Novu is enabled */ isEnabled(): boolean { return this.isNovuEnabled; } } export const notificationService = new NotificationService();