Files
thrilltrack-explorer/src/lib/notificationService.ts
gpt-engineer-app[bot] 81fccdc4d0 Fix remaining catch blocks
2025-10-21 17:08:24 +00:00

460 lines
13 KiB
TypeScript

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<boolean> {
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<NotificationPreferences | null> {
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<NotificationTemplate[]> {
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<void> {
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();