notifications

This commit is contained in:
alma 2025-05-04 11:25:36 +02:00
parent 509f9ea0aa
commit fc1dd882b6
3 changed files with 346 additions and 74 deletions

View File

@ -0,0 +1,83 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { NotificationService } from '@/lib/services/notifications/notification-service';
// GET /api/debug/notifications
export async function GET(request: Request) {
try {
// Authenticate user
const session = await getServerSession(authOptions);
if (!session || !session.user?.id) {
return NextResponse.json(
{ error: "Not authenticated" },
{ status: 401 }
);
}
const userId = session.user.id;
console.log(`[DEBUG] Testing notifications for user ${userId}`);
// Get environment variables status
const envStatus = {
LEANTIME_API_URL: process.env.LEANTIME_API_URL ? 'Set' : 'Not set',
LEANTIME_TOKEN: process.env.LEANTIME_TOKEN ? `Set (length: ${process.env.LEANTIME_TOKEN.length})` : 'Not set',
LEANTIME_API_KEY: process.env.LEANTIME_API_KEY ? `Set (length: ${process.env.LEANTIME_API_KEY.length})` : 'Not set',
KEYCLOAK_BASE_URL: process.env.KEYCLOAK_BASE_URL ? 'Set' : 'Not set',
KEYCLOAK_REALM: process.env.KEYCLOAK_REALM || 'Not set',
KEYCLOAK_ADMIN_CLIENT_ID: process.env.KEYCLOAK_ADMIN_CLIENT_ID ? 'Set' : 'Not set',
KEYCLOAK_ADMIN_CLIENT_SECRET: process.env.KEYCLOAK_ADMIN_CLIENT_SECRET ? 'Set (masked)' : 'Not set',
};
// Get user information
console.log(`[DEBUG] Getting user info for ${userId}`);
let userInfo = {
id: userId,
email: session.user.email || 'Unknown'
};
// Test notification service
console.log(`[DEBUG] Testing notification service`);
const notificationService = NotificationService.getInstance();
// Get notification count
console.log(`[DEBUG] Getting notification count`);
const startTimeCount = Date.now();
const notificationCount = await notificationService.getNotificationCount(userId);
const timeForCount = Date.now() - startTimeCount;
// Get notifications
console.log(`[DEBUG] Getting notifications`);
const startTimeNotifications = Date.now();
const notifications = await notificationService.getNotifications(userId, 1, 10);
const timeForNotifications = Date.now() - startTimeNotifications;
return NextResponse.json({
success: true,
timestamp: new Date().toISOString(),
userInfo,
environmentVariables: envStatus,
notificationServiceTest: {
count: {
result: notificationCount,
timeMs: timeForCount
},
notifications: {
count: notifications.length,
timeMs: timeForNotifications,
samples: notifications.slice(0, 3) // Only return first 3 notifications as samples
}
}
});
} catch (error: any) {
console.error('[DEBUG] Error in debug notifications API:', error);
return NextResponse.json(
{
error: "Internal server error",
message: error.message,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
},
{ status: 500 }
);
}
}

View File

@ -23,20 +23,33 @@ export class LeantimeAdapter implements NotificationAdapter {
// Load from environment variables, matching the pattern used in other Leantime integrations
this.apiUrl = process.env.LEANTIME_API_URL || '';
this.apiToken = process.env.LEANTIME_TOKEN || '';
// Log configuration on initialization
console.log('[LEANTIME_ADAPTER] Initialized with:', {
apiUrlConfigured: !!this.apiUrl,
apiTokenConfigured: !!this.apiToken,
apiUrlPrefix: this.apiUrl ? this.apiUrl.substring(0, 30) + '...' : 'not set',
});
}
async getNotifications(userId: string, page = 1, limit = 20): Promise<Notification[]> {
console.log(`[LEANTIME_ADAPTER] getNotifications called for userId: ${userId}, page: ${page}, limit: ${limit}`);
try {
// First get the Leantime user ID from email
const email = await this.getUserEmail(userId);
console.log(`[LEANTIME_ADAPTER] Retrieved email for userId ${userId}:`, email || 'null');
if (!email) {
console.error('Could not get user email for userId:', userId);
console.error('[LEANTIME_ADAPTER] Could not get user email for userId:', userId);
return [];
}
const leantimeUserId = await this.getLeantimeUserId(email);
console.log(`[LEANTIME_ADAPTER] Retrieved Leantime userId for email ${email}:`, leantimeUserId || 'null');
if (!leantimeUserId) {
console.error('User not found in Leantime:', email);
console.error('[LEANTIME_ADAPTER] User not found in Leantime:', email);
return [];
}
@ -44,88 +57,137 @@ export class LeantimeAdapter implements NotificationAdapter {
const offset = (page - 1) * limit;
// Make request to Leantime API using jsonrpc
console.log('[LEANTIME_ADAPTER] Sending request to Leantime API for notifications');
const jsonRpcBody = {
jsonrpc: '2.0',
method: 'leantime.rpc.notifications.getAll',
params: {
userId: leantimeUserId,
limit: limit,
offset: offset
},
id: 1
};
console.log('[LEANTIME_ADAPTER] Request body:', JSON.stringify(jsonRpcBody));
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.apiToken
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'leantime.rpc.notifications.getAll',
params: {
userId: leantimeUserId,
limit: limit,
offset: offset
},
id: 1
})
body: JSON.stringify(jsonRpcBody)
});
console.log('[LEANTIME_ADAPTER] Response status:', response.status);
if (!response.ok) {
console.error(`Failed to fetch Leantime notifications: ${response.status}`);
const errorText = await response.text();
console.error('[LEANTIME_ADAPTER] Failed to fetch Leantime notifications:', {
status: response.status,
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
});
return [];
}
const data = await response.json();
const responseText = await response.text();
console.log('[LEANTIME_ADAPTER] Raw response:', responseText.substring(0, 200) + (responseText.length > 200 ? '...' : ''));
const data = JSON.parse(responseText);
console.log('[LEANTIME_ADAPTER] Parsed response data:', {
hasResult: !!data.result,
resultIsArray: Array.isArray(data.result),
resultLength: Array.isArray(data.result) ? data.result.length : 'n/a',
error: data.error
});
if (!data.result || !Array.isArray(data.result)) {
console.error('Invalid response format from Leantime notifications API');
console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime notifications API');
return [];
}
return this.transformNotifications(data.result, userId);
const notifications = this.transformNotifications(data.result, userId);
console.log('[LEANTIME_ADAPTER] Transformed notifications count:', notifications.length);
return notifications;
} catch (error) {
console.error('Error fetching Leantime notifications:', error);
console.error('[LEANTIME_ADAPTER] Error fetching Leantime notifications:', error);
return [];
}
}
async getNotificationCount(userId: string): Promise<NotificationCount> {
console.log(`[LEANTIME_ADAPTER] getNotificationCount called for userId: ${userId}`);
try {
// First get the Leantime user ID from email
const email = await this.getUserEmail(userId);
console.log(`[LEANTIME_ADAPTER] Retrieved email for userId ${userId}:`, email || 'null');
if (!email) {
console.error('Could not get user email for userId:', userId);
console.error('[LEANTIME_ADAPTER] Could not get user email for userId:', userId);
return this.getEmptyCount();
}
const leantimeUserId = await this.getLeantimeUserId(email);
console.log(`[LEANTIME_ADAPTER] Retrieved Leantime userId for email ${email}:`, leantimeUserId || 'null');
if (!leantimeUserId) {
console.error('User not found in Leantime:', email);
console.error('[LEANTIME_ADAPTER] User not found in Leantime:', email);
return this.getEmptyCount();
}
// Make request to Leantime API using jsonrpc
console.log('[LEANTIME_ADAPTER] Sending request to Leantime API for notification count');
const jsonRpcBody = {
jsonrpc: '2.0',
method: 'leantime.rpc.notifications.getNotificationCount',
params: {
userId: leantimeUserId
},
id: 1
};
console.log('[LEANTIME_ADAPTER] Request body:', JSON.stringify(jsonRpcBody));
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.apiToken
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'leantime.rpc.notifications.getNotificationCount',
params: {
userId: leantimeUserId
},
id: 1
})
body: JSON.stringify(jsonRpcBody)
});
console.log('[LEANTIME_ADAPTER] Response status:', response.status);
if (!response.ok) {
console.error(`Failed to fetch Leantime notification count: ${response.status}`);
const errorText = await response.text();
console.error('[LEANTIME_ADAPTER] Failed to fetch Leantime notification count:', {
status: response.status,
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
});
return this.getEmptyCount();
}
const data = await response.json();
const responseText = await response.text();
console.log('[LEANTIME_ADAPTER] Raw response:', responseText);
const data = JSON.parse(responseText);
console.log('[LEANTIME_ADAPTER] Parsed response data:', {
hasResult: !!data.result,
resultData: data.result,
error: data.error
});
if (!data.result) {
console.error('Invalid response format from Leantime notification count API');
console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime notification count API');
return this.getEmptyCount();
}
const unreadCount = data.result.unread || 0;
const totalCount = data.result.total || 0;
console.log('[LEANTIME_ADAPTER] Notification counts:', { unread: unreadCount, total: totalCount });
return {
total: totalCount,
unread: unreadCount,
@ -137,7 +199,7 @@ export class LeantimeAdapter implements NotificationAdapter {
}
};
} catch (error) {
console.error('Error fetching Leantime notification count:', error);
console.error('[LEANTIME_ADAPTER] Error fetching Leantime notification count:', error);
return this.getEmptyCount();
}
}
@ -267,36 +329,69 @@ export class LeantimeAdapter implements NotificationAdapter {
// Helper function to get user's email from ID (using existing method from your system)
private async getUserEmail(userId: string): Promise<string | null> {
console.log(`[LEANTIME_ADAPTER] Getting email for userId: ${userId}`);
try {
// Fetch from Keycloak similar to how other components do
const response = await fetch(`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${userId}`, {
const adminToken = await this.getAdminToken();
console.log(`[LEANTIME_ADAPTER] Got admin token for Keycloak:`, adminToken ? 'token obtained' : 'failed to get token');
if (!adminToken) {
console.error('[LEANTIME_ADAPTER] Failed to get admin token for Keycloak');
return null;
}
const keycloakUrl = `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${userId}`;
console.log(`[LEANTIME_ADAPTER] Fetching user from Keycloak:`, keycloakUrl);
const response = await fetch(keycloakUrl, {
headers: {
'Authorization': `Bearer ${await this.getAdminToken()}`
'Authorization': `Bearer ${adminToken}`
}
});
console.log(`[LEANTIME_ADAPTER] Keycloak response status:`, response.status);
if (!response.ok) {
console.error('Failed to get user from Keycloak');
const errorText = await response.text();
console.error('[LEANTIME_ADAPTER] Failed to get user from Keycloak:', {
status: response.status,
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
});
return null;
}
const userData = await response.json();
console.log(`[LEANTIME_ADAPTER] Got user data from Keycloak:`, {
id: userData.id,
email: userData.email,
username: userData.username
});
return userData.email || null;
} catch (error) {
console.error('Error getting user email:', error);
console.error('[LEANTIME_ADAPTER] Error getting user email:', error);
return null;
}
}
// Helper function to get Leantime user ID by email (using similar pattern to the tasks API)
private async getLeantimeUserId(email: string): Promise<number | null> {
console.log(`[LEANTIME_ADAPTER] Getting Leantime userId for email: ${email}`);
try {
if (!this.apiToken) {
console.error('LEANTIME_TOKEN is not set in environment variables');
console.error('[LEANTIME_ADAPTER] LEANTIME_TOKEN is not set in environment variables');
return null;
}
console.log('Fetching Leantime user for email:', email);
console.log('[LEANTIME_ADAPTER] Sending request to get all Leantime users');
const jsonRpcBody = {
jsonrpc: '2.0',
method: 'leantime.rpc.users.getAll',
id: 1
};
console.log('[LEANTIME_ADAPTER] Request body:', JSON.stringify(jsonRpcBody));
const response = await fetch(`${this.apiUrl}/api/jsonrpc`, {
method: 'POST',
@ -304,46 +399,85 @@ export class LeantimeAdapter implements NotificationAdapter {
'Content-Type': 'application/json',
'X-API-Key': this.apiToken
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'leantime.rpc.users.getAll',
id: 1
}),
body: JSON.stringify(jsonRpcBody),
});
console.log('[LEANTIME_ADAPTER] Response status:', response.status);
if (!response.ok) {
console.error('Failed to fetch Leantime users');
const errorText = await response.text();
console.error('[LEANTIME_ADAPTER] Failed to fetch Leantime users:', {
status: response.status,
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
});
return null;
}
const data = await response.json();
const responseText = await response.text();
console.log('[LEANTIME_ADAPTER] Raw response (truncated):', responseText.substring(0, 200) + (responseText.length > 200 ? '...' : ''));
const data = JSON.parse(responseText);
console.log('[LEANTIME_ADAPTER] Parsed response data:', {
hasResult: !!data.result,
resultIsArray: Array.isArray(data.result),
resultLength: Array.isArray(data.result) ? data.result.length : 'n/a',
error: data.error
});
if (!data.result || !Array.isArray(data.result)) {
console.error('Invalid response format from Leantime users API');
console.error('[LEANTIME_ADAPTER] Invalid response format from Leantime users API');
return null;
}
const users = data.result;
console.log(`[LEANTIME_ADAPTER] Searching for user with email ${email} among ${users.length} users`);
// Log a few user objects to check structure
if (users.length > 0) {
console.log('[LEANTIME_ADAPTER] Sample user object:', JSON.stringify(users[0]));
}
const user = users.find((u: any) => u.username === email);
if (user) {
console.log('Found Leantime user:', { id: user.id, username: user.username });
console.log('[LEANTIME_ADAPTER] Found Leantime user:', { id: user.id, username: user.username });
return user.id;
} else {
console.log('No Leantime user found for email:', email);
console.log('[LEANTIME_ADAPTER] No Leantime user found for email:', email);
// Try alternative property names that might contain the email
const alternativeUser = users.find((u: any) =>
u.email === email ||
u.mail === email ||
(typeof u.username === 'string' && u.username.toLowerCase() === email.toLowerCase())
);
if (alternativeUser) {
console.log('[LEANTIME_ADAPTER] Found Leantime user with alternative property match:', {
id: alternativeUser.id,
username: alternativeUser.username,
email: alternativeUser.email || alternativeUser.mail
});
return alternativeUser.id;
}
return null;
}
} catch (error) {
console.error('Error getting Leantime user ID:', error);
console.error('[LEANTIME_ADAPTER] Error getting Leantime user ID:', error);
return null;
}
}
// Helper function to get admin token (similar to the user management API)
private async getAdminToken(): Promise<string | null> {
console.log('[LEANTIME_ADAPTER] Getting admin token from Keycloak');
try {
const keycloakTokenUrl = `${process.env.KEYCLOAK_BASE_URL}/realms/master/protocol/openid-connect/token`;
console.log('[LEANTIME_ADAPTER] Keycloak token URL:', keycloakTokenUrl);
const response = await fetch(
`${process.env.KEYCLOAK_BASE_URL}/realms/master/protocol/openid-connect/token`,
keycloakTokenUrl,
{
method: 'POST',
headers: {
@ -357,15 +491,23 @@ export class LeantimeAdapter implements NotificationAdapter {
}
);
console.log('[LEANTIME_ADAPTER] Keycloak token response status:', response.status);
if (!response.ok) {
console.error('Failed to get admin token');
const errorText = await response.text();
console.error('[LEANTIME_ADAPTER] Failed to get admin token:', {
status: response.status,
body: errorText.substring(0, 200) + (errorText.length > 200 ? '...' : '')
});
return null;
}
const tokenData = await response.json();
console.log('[LEANTIME_ADAPTER] Successfully obtained admin token');
return tokenData.access_token;
} catch (error) {
console.error('Error getting admin token:', error);
console.error('[LEANTIME_ADAPTER] Error getting admin token:', error);
return null;
}
}

View File

@ -17,6 +17,8 @@ export class NotificationService {
private static REFRESH_LOCK_TTL = 30; // 30 seconds
constructor() {
console.log('[NOTIFICATION_SERVICE] Initializing notification service');
// Register adapters
this.registerAdapter(new LeantimeAdapter());
@ -25,6 +27,8 @@ export class NotificationService {
// this.registerAdapter(new GiteaAdapter());
// this.registerAdapter(new DolibarrAdapter());
// this.registerAdapter(new MoodleAdapter());
console.log('[NOTIFICATION_SERVICE] Registered adapters:', Array.from(this.adapters.keys()));
}
/**
@ -32,6 +36,7 @@ export class NotificationService {
*/
public static getInstance(): NotificationService {
if (!NotificationService.instance) {
console.log('[NOTIFICATION_SERVICE] Creating new notification service instance');
NotificationService.instance = new NotificationService();
}
return NotificationService.instance;
@ -42,13 +47,14 @@ export class NotificationService {
*/
private registerAdapter(adapter: NotificationAdapter): void {
this.adapters.set(adapter.sourceName, adapter);
console.log(`Registered notification adapter: ${adapter.sourceName}`);
console.log(`[NOTIFICATION_SERVICE] Registered notification adapter: ${adapter.sourceName}`);
}
/**
* Get all notifications for a user from all configured sources
*/
async getNotifications(userId: string, page = 1, limit = 20): Promise<Notification[]> {
console.log(`[NOTIFICATION_SERVICE] getNotifications called for user ${userId}, page ${page}, limit ${limit}`);
const redis = getRedisClient();
const cacheKey = NotificationService.NOTIFICATIONS_LIST_CACHE_KEY(userId, page, limit);
@ -67,31 +73,49 @@ export class NotificationService {
return JSON.parse(cachedData);
}
} catch (error) {
console.error('Error retrieving notifications from cache:', error);
console.error('[NOTIFICATION_SERVICE] Error retrieving notifications from cache:', error);
}
// No cached data, fetch from all adapters
console.log(`[NOTIFICATION_SERVICE] Fetching notifications for user ${userId}`);
console.log(`[NOTIFICATION_SERVICE] Fetching notifications for user ${userId} from ${this.adapters.size} adapters`);
const allNotifications: Notification[] = [];
const promises = Array.from(this.adapters.values())
.map(adapter => adapter.isConfigured()
.then(configured => configured ? adapter.getNotifications(userId, page, limit) : [])
.catch(error => {
console.error(`Error fetching notifications from ${adapter.sourceName}:`, error);
const adapterEntries = Array.from(this.adapters.entries());
console.log(`[NOTIFICATION_SERVICE] Available adapters: ${adapterEntries.map(([name]) => name).join(', ')}`);
const promises = adapterEntries.map(async ([name, adapter]) => {
console.log(`[NOTIFICATION_SERVICE] Checking if adapter ${name} is configured`);
try {
const configured = await adapter.isConfigured();
console.log(`[NOTIFICATION_SERVICE] Adapter ${name} is configured: ${configured}`);
if (configured) {
console.log(`[NOTIFICATION_SERVICE] Fetching notifications from ${name} for user ${userId}`);
const notifications = await adapter.getNotifications(userId, page, limit);
console.log(`[NOTIFICATION_SERVICE] Got ${notifications.length} notifications from ${name}`);
return notifications;
} else {
console.log(`[NOTIFICATION_SERVICE] Skipping adapter ${name} as it is not configured`);
return [];
})
);
}
} catch (error) {
console.error(`[NOTIFICATION_SERVICE] Error fetching notifications from ${name}:`, error);
return [];
}
});
const results = await Promise.all(promises);
// Combine all notifications
results.forEach(notifications => {
results.forEach((notifications, index) => {
const adapterName = adapterEntries[index][0];
console.log(`[NOTIFICATION_SERVICE] Adding ${notifications.length} notifications from ${adapterName}`);
allNotifications.push(...notifications);
});
// Sort by timestamp (newest first)
allNotifications.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
console.log(`[NOTIFICATION_SERVICE] Total notifications after sorting: ${allNotifications.length}`);
// Store in cache
try {
@ -101,8 +125,9 @@ export class NotificationService {
'EX',
NotificationService.LIST_CACHE_TTL
);
console.log(`[NOTIFICATION_SERVICE] Cached ${allNotifications.length} notifications for user ${userId}`);
} catch (error) {
console.error('Error caching notifications:', error);
console.error('[NOTIFICATION_SERVICE] Error caching notifications:', error);
}
return allNotifications;
@ -112,6 +137,7 @@ export class NotificationService {
* Get notification counts for a user
*/
async getNotificationCount(userId: string): Promise<NotificationCount> {
console.log(`[NOTIFICATION_SERVICE] getNotificationCount called for user ${userId}`);
const redis = getRedisClient();
const cacheKey = NotificationService.NOTIFICATION_COUNT_CACHE_KEY(userId);
@ -130,11 +156,11 @@ export class NotificationService {
return JSON.parse(cachedData);
}
} catch (error) {
console.error('Error retrieving notification counts from cache:', error);
console.error('[NOTIFICATION_SERVICE] Error retrieving notification counts from cache:', error);
}
// No cached data, fetch counts from all adapters
console.log(`[NOTIFICATION_SERVICE] Fetching notification counts for user ${userId}`);
console.log(`[NOTIFICATION_SERVICE] Fetching notification counts for user ${userId} from ${this.adapters.size} adapters`);
const aggregatedCount: NotificationCount = {
total: 0,
@ -142,21 +168,39 @@ export class NotificationService {
sources: {}
};
const promises = Array.from(this.adapters.values())
.map(adapter => adapter.isConfigured()
.then(configured => configured ? adapter.getNotificationCount(userId) : null)
.catch(error => {
console.error(`Error fetching notification count from ${adapter.sourceName}:`, error);
const adapterEntries = Array.from(this.adapters.entries());
console.log(`[NOTIFICATION_SERVICE] Available adapters for count: ${adapterEntries.map(([name]) => name).join(', ')}`);
const promises = adapterEntries.map(async ([name, adapter]) => {
console.log(`[NOTIFICATION_SERVICE] Checking if adapter ${name} is configured for count`);
try {
const configured = await adapter.isConfigured();
console.log(`[NOTIFICATION_SERVICE] Adapter ${name} is configured for count: ${configured}`);
if (configured) {
console.log(`[NOTIFICATION_SERVICE] Fetching notification count from ${name} for user ${userId}`);
const count = await adapter.getNotificationCount(userId);
console.log(`[NOTIFICATION_SERVICE] Got count from ${name}:`, count);
return count;
} else {
console.log(`[NOTIFICATION_SERVICE] Skipping adapter ${name} for count as it is not configured`);
return null;
})
);
}
} catch (error) {
console.error(`[NOTIFICATION_SERVICE] Error fetching notification count from ${name}:`, error);
return null;
}
});
const results = await Promise.all(promises);
// Combine all counts
results.forEach(count => {
results.forEach((count, index) => {
if (!count) return;
const adapterName = adapterEntries[index][0];
console.log(`[NOTIFICATION_SERVICE] Adding counts from ${adapterName}: total=${count.total}, unread=${count.unread}`);
aggregatedCount.total += count.total;
aggregatedCount.unread += count.unread;
@ -166,6 +210,8 @@ export class NotificationService {
});
});
console.log(`[NOTIFICATION_SERVICE] Aggregated counts for user ${userId}:`, aggregatedCount);
// Store in cache
try {
await redis.set(
@ -174,8 +220,9 @@ export class NotificationService {
'EX',
NotificationService.COUNT_CACHE_TTL
);
console.log(`[NOTIFICATION_SERVICE] Cached notification counts for user ${userId}`);
} catch (error) {
console.error('Error caching notification counts:', error);
console.error('[NOTIFICATION_SERVICE] Error caching notification counts:', error);
}
return aggregatedCount;