Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import { requestLoggerMiddleware } from './middleware/requestLogger';
import { schedulerService } from './services/scheduler';
import { reminderEngine } from './services/reminder-engine';
import { notificationPreferenceService } from './services/notification-preference-service';

Check failure on line 26 in backend/src/index.ts

View workflow job for this annotation

GitHub Actions / Lint Backend

'notificationPreferenceService' is defined but never used
import subscriptionRoutes from './routes/subscriptions';
import riskScoreRoutes from './routes/risk-score';
import simulationRoutes from './routes/simulation';
Expand All @@ -35,6 +35,7 @@
import tagsRoutes from './routes/tags';
import userRoutes from './routes/user';
import userPreferencesRoutes from './routes/user-preferences';
import reminderSettingsRoutes from './routes/reminder-settings';
import apiKeysRoutes from './routes/api-keys';
import digestRoutes from './routes/digest';
import mfaRoutes from './routes/mfa';
Expand Down Expand Up @@ -130,6 +131,7 @@
app.use('/api/tags', tagsRoutes);
app.use('/api/user', userRoutes);
app.use('/api/user-preferences', authenticate, userPreferencesRoutes);
app.use('/api/reminder-settings', authenticate, reminderSettingsRoutes);
app.use('/api/digest', digestRoutes);
app.use('/api/mfa', mfaRoutes);
app.use('/api/notifications/push', pushNotificationRoutes);
Expand Down
58 changes: 58 additions & 0 deletions backend/src/routes/reminder-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Router, Response } from 'express';
import { AuthenticatedRequest } from '../middleware/auth';
import { validateRequest } from '../utils/validation';
import { reminderSettingsService } from '../services/reminder-settings-service';
import logger from '../config/logger';
import { z } from 'zod';

const reminderSettingsUpdateSchema = z.object({
reminder_days_before: z.array(z.number().int().min(1).max(365)).optional(),
});

const router = Router();

/**
* GET /api/reminder-settings
* Get current reminder settings
*/
router.get('/', async (req: AuthenticatedRequest, res: Response) => {
try {
const settings = await reminderSettingsService.getSettings(req.user!.id);
res.json({ success: true, data: settings });
} catch (error) {
logger.error('Error fetching reminder settings:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch reminder settings'
});
}
});

/**
* PATCH /api/reminder-settings
* Update reminder settings
*/
router.patch('/', async (req: AuthenticatedRequest, res: Response) => {
try {
const validatedData = validateRequest(reminderSettingsUpdateSchema, req.body);

const updatedSettings = await reminderSettingsService.updateSettings(
req.user!.id,
validatedData
);

res.json({
success: true,
data: updatedSettings,
message: 'Reminder settings updated successfully'
});
} catch (error) {
logger.error('Error updating reminder settings:', error);
res.status(500).json({
success: false,
error: 'Failed to update reminder settings'
});
}
});

export default router;
4 changes: 3 additions & 1 deletion backend/src/services/reminder-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { calculateBackoffDelay } from '../utils/retry';
import { userPreferenceService } from './user-preference-service';
import { notificationPreferenceService } from './notification-preference-service';
import { reminderSettingsService } from './reminder-settings-service';
import { quietHoursService } from './quiet-hours-service';
import { delayedNotificationService } from './delayed-notification-service';

Expand Down Expand Up @@ -639,8 +640,9 @@ export class ReminderEngine {
// 2. User global settings
try {
const userPrefs = await userPreferenceService.getPreferences(userId);
const reminderSettings = await reminderSettingsService.getSettings(userId);
return {
reminder_days_before: userPrefs.reminder_timing ?? this.defaultDaysBefore,
reminder_days_before: reminderSettings.reminder_days_before,
channels: userPrefs.notification_channels ?? ['email'],
muted: false,
};
Expand Down
101 changes: 101 additions & 0 deletions backend/src/services/reminder-settings-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { supabase } from '../config/database';
import logger from '../config/logger';

export interface ReminderSettings {
user_id: string;
reminder_days_before: number[];
created_at: string;
updated_at: string;
}

export interface PartialReminderSettings {
reminder_days_before?: number[];
}

export class ReminderSettingsService {
private readonly defaultSettings: Omit<ReminderSettings, 'user_id' | 'created_at' | 'updated_at'> = {
reminder_days_before: [7, 3, 1],
};

/**
* Get reminder settings, returning defaults if not found
*/
async getSettings(userId: string): Promise<ReminderSettings> {
try {
const { data, error } = await supabase
.from('reminder_settings')
.select('*')
.eq('user_id', userId)
.single();

if (error && error.code !== 'PGRST116') {
// PGRST116 is "no rows returned"
logger.error(`Error fetching reminder settings for user ${userId}:`, error);
throw error;
}

if (!data) {
return {
user_id: userId,
...this.defaultSettings,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
}

return data as ReminderSettings;
} catch (error) {
logger.error(`Unexpected error fetching reminder settings for user ${userId}:`, error);
return {
user_id: userId,
...this.defaultSettings,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
}
}

/**
* Update reminder settings partially
*/
async updateSettings(
userId: string,
updates: PartialReminderSettings
): Promise<ReminderSettings> {
try {
// Fetch current settings to ensure safe merging
const current = await this.getSettings(userId);

const merged: Partial<ReminderSettings> = {
...current,
...updates,
};

// Remove keys that shouldn't be updated directly
delete merged.user_id;
delete (merged as any).created_at;
delete (merged as any).updated_at;

const { data, error } = await supabase
.from('reminder_settings')
.upsert({
user_id: userId,
...merged,
})
.select()
.single();

if (error) {
logger.error(`Error updating reminder settings for user ${userId}:`, error);
throw error;
}

return data as ReminderSettings;
} catch (error) {
logger.error(`Unexpected error updating reminder settings for user ${userId}:`, error);
throw error;
}
}
}

export const reminderSettingsService = new ReminderSettingsService();
7 changes: 7 additions & 0 deletions backend/src/types/reminder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,12 @@ export interface UserPreferences {
updated_at: string;
}

export interface ReminderSettings {
user_id: string;
reminder_days_before: number[];
created_at: string;
updated_at: string;
}

export type PartialUserPreferences = Partial<Omit<UserPreferences, 'user_id' | 'updated_at'>>;

8 changes: 4 additions & 4 deletions backend/tests/reminder-engine-batch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ describe('ReminderEngine Batch Optimization', () => {
];

const mockPreferences = [
{ user_id: 'user1', reminder_timing: [7, 3] },
{ user_id: 'user2', reminder_timing: [1] },
{ user_id: 'user1', reminder_days_before: [7, 3] },
{ user_id: 'user2', reminder_days_before: [1] },
];

// Setup mocks
Expand All @@ -54,7 +54,7 @@ describe('ReminderEngine Batch Optimization', () => {
}),
};
}
if (table === 'user_preferences') {
if (table === 'reminder_settings') {
return {
select: () => ({
in: () => Promise.resolve({ data: mockPreferences, error: null }),
Expand All @@ -71,7 +71,7 @@ describe('ReminderEngine Batch Optimization', () => {
await engine.scheduleReminders([7, 3, 1]);

// Verify batch fetch of preferences
expect(supabase.from).toHaveBeenCalledWith('user_preferences');
expect(supabase.from).toHaveBeenCalledWith('reminder_settings');

// Verify batch upsert
expect(supabase.from).toHaveBeenCalledWith('reminder_schedules');
Expand Down
6 changes: 6 additions & 0 deletions client/app/settings/notifications/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { redirect } from "next/navigation"
import { createClient } from "@/lib/supabase/server"
import QuietHoursSettings from "@/components/settings/QuietHoursSettings"
import ReminderSettings from "@/components/settings/ReminderSettings"
import Link from "next/link"

export default async function NotificationSettingsPage() {
Expand Down Expand Up @@ -59,6 +60,11 @@ export default async function NotificationSettingsPage() {
</Link>
</div>

{/* Reminder Settings */}
<div className="mb-8">
<ReminderSettings />
</div>

{/* Quiet Hours Settings */}
<QuietHoursSettings />
</div>
Expand Down
Loading
Loading