type, data->post_id, 1-hour bucket) */ class NotificationDigestService { /** * Aggregate a raw notification collection into digest entries. * * @param \Illuminate\Database\Eloquent\Collection $notifications * @return array */ public function aggregate(Collection $notifications): array { $groups = []; foreach ($notifications as $notif) { $data = is_array($notif->data) ? $notif->data : json_decode($notif->data, true); $type = $data['type'] ?? 'unknown'; $postId = $data['post_id'] ?? null; // 1-hour bucket $bucket = $notif->created_at->format('Y-m-d H'); $key = "{$type}:{$postId}:{$bucket}"; if (! isset($groups[$key])) { $groups[$key] = [ 'type' => $type, 'count' => 0, 'actors' => [], 'post_id' => $postId, 'url' => $data['url'] ?? null, 'latest_at' => $notif->created_at->toISOString(), 'read' => $notif->read_at !== null, 'ids' => [], ]; } $groups[$key]['count']++; $groups[$key]['ids'][] = $notif->id; // Collect unique actors (up to 5 for display) $actorId = $data['commenter_id'] ?? $data['reactor_id'] ?? $data['actor_id'] ?? null; if ($actorId && count($groups[$key]['actors']) < 5) { $alreadyAdded = collect($groups[$key]['actors'])->contains('id', $actorId); if (! $alreadyAdded) { $groups[$key]['actors'][] = [ 'id' => $actorId, 'name' => $data['commenter_name'] ?? $data['actor_name'] ?? null, 'username' => $data['commenter_username'] ?? $data['actor_username'] ?? null, ]; } } if ($notif->read_at === null) { $groups[$key]['read'] = false; // group is unread if any item is unread } } // Build readable message for each group $result = []; foreach ($groups as $group) { $group['message'] = $this->buildMessage($group); $result[] = $group; } // Sort by latest_at descending usort($result, fn ($a, $b) => strcmp($b['latest_at'], $a['latest_at'])); return $result; } private function buildMessage(array $group): string { $count = $group['count']; $actors = $group['actors']; $first = $actors[0]['name'] ?? 'Someone'; $others = $count - 1; return match ($group['type']) { 'post_commented' => $count === 1 ? "{$first} commented on your post" : "{$first} and {$others} other(s) commented on your post", 'post_liked' => $count === 1 ? "{$first} liked your post" : "{$first} and {$others} other(s) liked your post", 'post_shared' => $count === 1 ? "{$first} shared your artwork" : "{$first} and {$others} other(s) shared your artwork", default => $count === 1 ? "{$first} interacted with your post" : "{$count} people interacted with your post", }; } }