$notificationCode, 'message' => $message, 'title' => $title, ]); return 0; } // Get active users with target roles in single query // Performance: Uses whereIn for efficient filtering $users = DB::table('users') ->select('id', 'level', 'name', 'email') ->whereIn('level', $targetRoles) ->whereNotNull('level') // Ensure level is not null ->get(); // Remove level, name, email from selection after logging (we only need id for insert) $userIds = $users->pluck('id'); // Debug: Log all users found Log::info("Notification target users found", [ 'notification_code' => $notificationCode, 'target_roles' => $targetRoles, 'users_count' => $users->count(), 'user_ids' => $users->pluck('id')->toArray(), 'user_levels' => $users->pluck('level')->toArray(), ]); // If no users found, log warning and return early if ($users->isEmpty()) { // Debug: Check what users exist with these levels $allUsersWithLevels = DB::table('users') ->select('id', 'level', 'name', 'email') ->whereNotNull('level') ->get() ->groupBy('level'); Log::warning("Notification not sent: No users found with target roles", [ 'notification_code' => $notificationCode, 'target_roles' => $targetRoles, 'message' => $message, 'available_levels' => $allUsersWithLevels->keys()->toArray(), 'users_by_level' => $allUsersWithLevels->map(function($group) { return $group->pluck('id')->toArray(); })->toArray(), ]); return 0; } // Generate title if not provided if ($title === null) { $title = ucwords(str_replace('_', ' ', $notificationCode)); } // If filterParams provided, generate link to filtered list page // We'll update link after insert with notification ID $useFilteredList = !empty($filterParams); if ($useFilteredList && $link === null) { $link = '/admin/notifications/filtered-list'; // Will be updated with notification ID after insert } // Prepare bulk insert data // Performance: Single insert query for all notifications $now = now(); $notificationsData = []; foreach ($userIds as $userId) { $notificationsData[] = [ 'user_id' => $userId, 'notification_code' => $notificationCode, 'title' => $title, 'message' => $message, 'link' => $link, 'is_read' => false, 'filter_params' => $filterParams ? json_encode($filterParams) : null, 'item_count' => $itemCount, 'notification_type' => $filterParams ? 'batch' : 'single', 'created_at' => $now, 'updated_at' => $now, ]; } // Bulk insert notifications // Performance: One query instead of N queries if (!empty($notificationsData)) { // Split into chunks to avoid max packet size issues $chunks = array_chunk($notificationsData, 500); $totalInserted = 0; foreach ($chunks as $chunk) { // If using filtered list, we need to update links with notification IDs after insert if ($useFilteredList) { $ids = []; foreach ($chunk as $data) { $id = DB::table('notifications')->insertGetId($data); $ids[] = $id; } // Update links with notification IDs foreach ($ids as $id) { DB::table('notifications') ->where('id', $id) ->update(['link' => '/admin/notifications/filtered-list?id=' . $id]); } $totalInserted += count($chunk); } else { DB::table('notifications')->insert($chunk); $totalInserted += count($chunk); } } return $totalInserted; } return 0; } catch (\Exception $e) { // Log error but don't break the main flow Log::error('Notification send error: ' . $e->getMessage(), [ 'code' => $notificationCode, 'message' => $message, ]); return 0; } } /** * Helper: Send batch notification with filter params and count-based message * * @param string $notificationCode * @param string $title * @param string $messageTemplate String with %d placeholder for count * @param array $filterParams * @param int $count * @return int */ function sendBatchNotificationWithFilter($notificationCode, $title, $messageTemplate, array $filterParams, int $count) { $count = (int) $count; if ($count <= 0) { return 0; } $message = $messageTemplate ? sprintf($messageTemplate, $count) : sprintf('%d record(s) require attention.', $count); return sendNotification($notificationCode, $message, null, $title, $filterParams, $count); } /** * Send notification to specific users (by user IDs) * Performance: Bulk insert for multiple users * * @param array $userIds Array of user IDs * @param string $notificationCode Notification code * @param string $message Notification message * @param string|null $link Optional link * @param string|null $title Optional title * @return int Number of notifications created */ function sendNotificationToUsers($userIds, $notificationCode, $message, $link = null, $title = null) { try { if (empty($userIds)) { return 0; } // Generate title if not provided if ($title === null) { $title = ucwords(str_replace('_', ' ', $notificationCode)); } // Prepare bulk insert data $now = now(); $notificationsData = []; foreach ($userIds as $userId) { $notificationsData[] = [ 'user_id' => $userId, 'notification_code' => $notificationCode, 'title' => $title, 'message' => $message, 'link' => $link, 'is_read' => false, 'created_at' => $now, 'updated_at' => $now, ]; } // Bulk insert if (!empty($notificationsData)) { $chunks = array_chunk($notificationsData, 500); $totalInserted = 0; foreach ($chunks as $chunk) { DB::table('notifications')->insert($chunk); $totalInserted += count($chunk); } return $totalInserted; } return 0; } catch (\Exception $e) { Log::error('Notification send error (specific users): ' . $e->getMessage()); return 0; } } /** * Send notification to single user * Performance: Single insert query * * @param int $userId User ID * @param string $notificationCode Notification code * @param string $message Notification message * @param string|null $link Optional link * @param string|null $title Optional title * @return bool Success status */ function sendNotificationToUser($userId, $notificationCode, $message, $link = null, $title = null) { try { if ($title === null) { $title = ucwords(str_replace('_', ' ', $notificationCode)); } Notification::create([ 'user_id' => $userId, 'notification_code' => $notificationCode, 'title' => $title, 'message' => $message, 'link' => $link, 'is_read' => false, ]); return true; } catch (\Exception $e) { Log::error('Notification send error (single user): ' . $e->getMessage()); return false; } } /** * Clean up old notifications * Performance: Bulk delete query with date filter * * @param int $days Number of days to keep (default: 90) * @return int Number of deleted notifications */ function cleanupOldNotifications($days = 90) { try { return Notification::deleteOldNotifications($days); } catch (\Exception $e) { Log::error('Notification cleanup error: ' . $e->getMessage()); return 0; } } /** * Copy existing notifications to new users based on their level * DISABLED: This function was causing duplicate notifications and system issues * * @param string $notificationCode Notification code to copy * @return int Always returns 0 (disabled) */ function copyNotificationsToNewUsers($notificationCode) { // DISABLED: This function was causing excessive duplicate notifications // All calls to this function should be removed from notification check commands return 0; } /** * Normalize filter_params for comparison * * @param mixed $filterParams * @return string */ function normalizeFilterParams($filterParams): string { if (empty($filterParams)) { return ''; } $decoded = is_string($filterParams) ? json_decode($filterParams, true) : $filterParams; if ($decoded === null || !is_array($decoded)) { return ''; } ksort($decoded); return json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } /** * Clean up duplicate notifications by notification_code + user_id + filter_params * This is more aggressive and handles cases where same notification_code is sent multiple times * Uses direct SQL queries with minimal memory usage * * @return array{total_deleted: int, notifications_before: int, notifications_after: int, error?: string} */ function cleanupDuplicateNotificationsByCode(): array { try { $stats = [ 'total_deleted' => 0, 'notifications_before' => 0, 'notifications_after' => 0, ]; $stats['notifications_before'] = DB::table('notifications')->count(); // Process in very small batches to avoid memory issues $batchSize = 10; // Process 10 users at a time $totalDeleted = 0; $usersAffected = []; // Get all notification codes one by one $notificationCodes = DB::table('notifications') ->select('notification_code') ->distinct() ->pluck('notification_code'); foreach ($notificationCodes as $notificationCode) { // Get user IDs for this code in batches $offset = 0; while (true) { $userIds = DB::table('notifications') ->where('notification_code', $notificationCode) ->select('user_id') ->distinct() ->offset($offset) ->limit($batchSize) ->pluck('user_id'); if ($userIds->isEmpty()) { break; } foreach ($userIds as $userId) { // Get notification IDs for this code + user, ordered by created_at desc // Process in small chunks $notificationOffset = 0; $notificationLimit = 100; $seenFilterParams = []; $idsToDelete = []; while (true) { $notifications = DB::table('notifications') ->where('notification_code', $notificationCode) ->where('user_id', $userId) ->select('id', 'filter_params', 'created_at') ->orderBy('created_at', 'desc') ->offset($notificationOffset) ->limit($notificationLimit) ->get(); if ($notifications->isEmpty()) { break; } foreach ($notifications as $notification) { // Normalize filter_params $filterParams = normalizeFilterParams($notification->filter_params); // If we've seen this filter_params before, it's a duplicate if (isset($seenFilterParams[$filterParams])) { $idsToDelete[] = $notification->id; } else { // First occurrence - keep it $seenFilterParams[$filterParams] = true; } } $notificationOffset += $notificationLimit; // If we got less than limit, we're done if ($notifications->count() < $notificationLimit) { break; } } // Delete duplicates for this user if (!empty($idsToDelete)) { $deleteChunks = array_chunk($idsToDelete, 500); foreach ($deleteChunks as $chunk) { $deleted = DB::table('notifications')->whereIn('id', $chunk)->delete(); $totalDeleted += $deleted; } if (!in_array($userId, $usersAffected)) { $usersAffected[] = $userId; } } // Free memory unset($notifications, $idsToDelete, $seenFilterParams); } $offset += $batchSize; } } // Clear cache for affected users foreach ($usersAffected as $userId) { Cache::forget("user_notifications_{$userId}"); Cache::forget("user_unread_type_count_{$userId}"); } $stats['total_deleted'] = $totalDeleted; $stats['notifications_after'] = DB::table('notifications')->count(); Log::info('Duplicate notifications cleanup by code completed', $stats); return $stats; } catch (\Exception $e) { Log::error('Error cleaning up duplicate notifications by code: ' . $e->getMessage()); return [ 'error' => $e->getMessage(), 'total_deleted' => 0, ]; } } /** * Clean up duplicate notifications * Removes duplicate notifications keeping only the most recent one for each user * Duplicates are identified by: user_id + message + link + normalized filter_params * * Performance: Uses SQL-based approach with chunking to avoid memory issues * * @return array{total_deleted: int, users_affected: int, notifications_before: int, notifications_after: int, error?: string} */ function cleanupDuplicateNotifications(): array { try { $stats = [ 'total_deleted' => 0, 'users_affected' => 0, 'notifications_before' => 0, 'notifications_after' => 0, ]; // Get total count before cleanup $stats['notifications_before'] = DB::table('notifications')->count(); // Process one user at a time to minimize memory usage $userIds = DB::table('notifications') ->select('user_id') ->distinct() ->pluck('user_id'); $usersAffected = []; $totalDeleted = 0; foreach ($userIds as $userId) { // Get notifications for this user, ordered by created_at desc (newest first) // Process in smaller chunks to avoid memory issues $offset = 0; $limit = 500; $userHasDuplicates = false; $seen = []; $idsToDelete = []; while (true) { $notifications = DB::table('notifications') ->where('user_id', $userId) ->select('id', 'message', 'link', 'filter_params', 'created_at') ->orderBy('created_at', 'desc') ->offset($offset) ->limit($limit) ->get(); if ($notifications->isEmpty()) { break; } foreach ($notifications as $notification) { // Normalize filter_params $filterParams = normalizeFilterParams($notification->filter_params); $key = $notification->message . '|' . ($notification->link ?? '') . '|' . $filterParams; // If we've seen this key before, it's a duplicate if (isset($seen[$key])) { $idsToDelete[] = $notification->id; $userHasDuplicates = true; } else { // First occurrence - keep it $seen[$key] = true; } } $offset += $limit; // If we got less than limit, we're done with this user if ($notifications->count() < $limit) { break; } } // Delete duplicates for this user if (!empty($idsToDelete)) { $deleteChunks = array_chunk($idsToDelete, 500); foreach ($deleteChunks as $chunk) { $deleted = DB::table('notifications')->whereIn('id', $chunk)->delete(); $totalDeleted += $deleted; } if ($userHasDuplicates) { $usersAffected[] = $userId; // Clear cache for this user Cache::forget("user_notifications_{$userId}"); Cache::forget("user_unread_type_count_{$userId}"); } } // Free memory unset($notifications, $idsToDelete, $seen); } $stats['total_deleted'] = $totalDeleted; $stats['users_affected'] = count($usersAffected); // Get total count after cleanup $stats['notifications_after'] = DB::table('notifications')->count(); Log::info('Duplicate notifications cleanup completed', $stats); return $stats; } catch (\Exception $e) { Log::error('Error cleaning up duplicate notifications: ' . $e->getMessage()); return [ 'error' => $e->getMessage(), 'total_deleted' => 0, ]; } }