Files
thrilltrack-explorer/src-old/lib/notificationService.ts

500 lines
14 KiB
TypeScript

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