add notifications logic and add support of both JWT and Sanctum

This commit is contained in:
root
2026-03-11 01:20:31 -04:00
parent f6be51576c
commit 182036cc41
141 changed files with 8685 additions and 648 deletions
@@ -0,0 +1,34 @@
<?php
namespace App\Services\Notifications;
use App\Models\Notification;
class NotificationActiveService
{
public function list(?string $targetGroup): array
{
$rows = Notification::getActiveNotifications($targetGroup);
$now = time();
$notifications = array_map(static function ($row) use ($now) {
$expiresAt = $row['expires_at'] ?? null;
$expiryTs = $expiresAt ? strtotime((string) $expiresAt) : false;
$isExpired = $expiryTs !== false && $expiryTs <= $now;
return [
'id' => $row['id'] ?? null,
'title' => $row['title'] ?? null,
'message' => $row['message'] ?? null,
'priority' => $row['priority'] ?? null,
'scheduled_at' => $row['scheduled_at'] ?? null,
'expires_at' => $expiresAt,
'created_at' => $row['created_at'] ?? null,
'target_group' => $row['target_group'] ?? null,
'isExpired' => $isExpired,
];
}, $rows ?? []);
return ['notifications' => $notifications];
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Services\Notifications;
use App\Models\Notification;
class NotificationCleanupService
{
public function cleanupExpired(): int
{
return Notification::deleteExpiredNotifications();
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Services\Notifications;
use App\Models\Notification;
class NotificationDeletedService
{
public function list(): array
{
$rows = Notification::getDeletedNotifications();
$notifications = array_map(static function ($row) {
return [
'id' => $row['id'] ?? null,
'title' => $row['title'] ?? null,
'message' => $row['message'] ?? null,
'priority' => $row['priority'] ?? null,
'scheduled_at' => $row['scheduled_at'] ?? null,
'expires_at' => $row['expires_at'] ?? null,
'deleted_at' => $row['deleted_at'] ?? null,
];
}, $rows ?? []);
return ['notifications' => $notifications];
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Services\Notifications;
use Illuminate\Support\Facades\Log;
class NotificationDispatchService
{
public function sendEmail(string $to, string $subject, string $message): void
{
Log::info('Notification email queued.', [
'to' => $to,
'subject' => $subject,
]);
}
public function sendSms(string $phone, string $message): void
{
Log::info('Notification SMS queued.', [
'phone' => $phone,
]);
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Services\Notifications;
use App\Models\Notification;
class NotificationManagementService
{
public function update(int $notificationId, array $payload): array
{
$notification = Notification::query()->find($notificationId);
if (!$notification) {
return ['ok' => false, 'message' => 'Notification not found.'];
}
$notification->update([
'title' => $payload['title'] ?? $notification->title,
'message' => $payload['message'] ?? $notification->message,
'target_group' => $payload['target_group'] ?? $notification->target_group,
'delivery_channels' => $payload['channels'] ?? $notification->delivery_channels,
'priority' => $payload['priority'] ?? $notification->priority,
'status' => $payload['status'] ?? $notification->status,
'action_url' => $payload['action_url'] ?? $notification->action_url,
'attachment_path' => $payload['attachment_path'] ?? $notification->attachment_path,
'scheduled_at' => $payload['scheduled_at'] ?? $notification->scheduled_at,
'expires_at' => $payload['expires_at'] ?? $notification->expires_at,
'updated_at' => utc_now(),
]);
return ['ok' => true, 'notification' => $notification];
}
public function delete(int $notificationId): bool
{
$notification = Notification::query()->find($notificationId);
if (!$notification) {
return false;
}
return (bool) $notification->delete();
}
public function restore(int $notificationId): bool
{
return Notification::restoreNotification($notificationId);
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Services\Notifications;
use App\Models\UserNotification;
class NotificationReadService
{
public function markRead(int $userId, int $notificationId): bool
{
$row = UserNotification::query()
->where('user_id', $userId)
->where('notification_id', $notificationId)
->first();
if (!$row) {
return false;
}
$row->is_read = true;
$row->save();
return true;
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Services\Notifications;
use App\Models\User;
class NotificationRecipientService
{
public function getRecipients(string $targetGroup): array
{
$rows = User::getUsersByRole($targetGroup);
return is_array($rows) ? $rows : [];
}
}
@@ -0,0 +1,84 @@
<?php
namespace App\Services\Notifications;
use App\Models\Notification;
use App\Models\UserNotification;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class NotificationSendService
{
public function __construct(
private NotificationRecipientService $recipients,
private NotificationDispatchService $dispatcher
) {
}
public function send(array $payload, int $actorId): array
{
$channels = $payload['channels'] ?? ['in_app'];
$channels = array_values(array_unique(array_map('strtolower', (array) $channels)));
if (empty($channels)) {
$channels = ['in_app'];
}
return DB::transaction(function () use ($payload, $channels, $actorId) {
$notification = Notification::query()->create([
'title' => $payload['title'],
'message' => $payload['message'],
'target_group' => $payload['target_group'],
'delivery_channels' => $channels,
'priority' => $payload['priority'] ?? null,
'status' => $payload['status'] ?? null,
'action_url' => $payload['action_url'] ?? null,
'attachment_path' => $payload['attachment_path'] ?? null,
'scheduled_at' => $payload['scheduled_at'] ?? now(),
'sent_at' => in_array('in_app', $channels, true) ? now() : null,
'expires_at' => $payload['expires_at'] ?? null,
'school_year' => $payload['school_year'] ?? null,
'semester' => $payload['semester'] ?? null,
'created_at' => utc_now(),
'updated_at' => utc_now(),
]);
$users = $this->recipients->getRecipients((string) $payload['target_group']);
$created = 0;
foreach ($users as $user) {
UserNotification::query()->create([
'notification_id' => (int) $notification->id,
'user_id' => (int) ($user['id'] ?? 0),
'is_read' => false,
'delivered' => in_array('in_app', $channels, true),
'delivered_at' => in_array('in_app', $channels, true) ? now() : null,
'school_year' => $payload['school_year'] ?? null,
'semester' => $payload['semester'] ?? null,
]);
$created++;
if (in_array('email', $channels, true) && !empty($user['email'])) {
$this->dispatcher->sendEmail((string) $user['email'], (string) $payload['title'], (string) $payload['message']);
}
if (in_array('sms', $channels, true) && !empty($user['phone'])) {
$this->dispatcher->sendSms((string) $user['phone'], (string) $payload['message']);
}
}
Log::info('Notification dispatched.', [
'notification_id' => (int) $notification->id,
'target_group' => $payload['target_group'],
'channels' => $channels,
'recipients' => $created,
'actor_id' => $actorId,
]);
return [
'ok' => true,
'notification' => $notification,
'recipient_count' => $created,
];
});
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Services\Notifications;
use App\Models\UserNotification;
class NotificationShowService
{
public function getForUser(int $userId, int $notificationId): ?array
{
$row = UserNotification::query()
->select([
'user_notifications.*',
'notifications.title',
'notifications.message',
'notifications.priority',
'notifications.scheduled_at',
'notifications.expires_at',
'notifications.target_group',
])
->join('notifications', 'notifications.id', '=', 'user_notifications.notification_id')
->where('user_notifications.user_id', $userId)
->where('user_notifications.notification_id', $notificationId)
->first();
return $row ? $row->toArray() : null;
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Services\Notifications;
use Illuminate\Support\Facades\Event;
class NotificationTriggerService
{
public static function trigger(string $eventType, array $payload): void
{
Event::dispatch($eventType, $payload);
}
public static function toUser(int $userId, string $title, string $message, array $channels = ['in_app']): void
{
self::trigger('customNotification', [
'user_id' => $userId,
'title' => $title,
'message' => $message,
'channels' => $channels,
]);
}
}
@@ -0,0 +1,61 @@
<?php
namespace App\Services\Notifications;
use App\Models\UserNotification;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class NotificationUserListService
{
public function listForUser(int $userId, array $filters): array
{
$perPage = (int) ($filters['per_page'] ?? 25);
$page = (int) ($filters['page'] ?? 1);
$sortBy = (string) ($filters['sort_by'] ?? 'created_at');
$sortDir = strtolower((string) ($filters['sort_dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc';
$allowedSorts = ['created_at', 'scheduled_at', 'priority'];
if (!in_array($sortBy, $allowedSorts, true)) {
$sortBy = 'created_at';
}
$query = UserNotification::query()
->select([
'user_notifications.*',
'notifications.title',
'notifications.message',
'notifications.priority',
'notifications.scheduled_at',
'notifications.expires_at',
'notifications.target_group',
])
->join('notifications', 'notifications.id', '=', 'user_notifications.notification_id')
->where('user_notifications.user_id', $userId);
if (isset($filters['read'])) {
$read = filter_var($filters['read'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if ($read !== null) {
$query->where('user_notifications.is_read', $read ? 1 : 0);
}
}
$paginator = $query
->orderBy('notifications.' . $sortBy, $sortDir)
->paginate($perPage, ['*'], 'page', $page);
return [
'notifications' => $paginator,
'pagination' => $this->paginationPayload($paginator),
];
}
private function paginationPayload(LengthAwarePaginator $paginator): array
{
return [
'current_page' => $paginator->currentPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
'total_pages' => $paginator->lastPage(),
];
}
}
@@ -0,0 +1,60 @@
<?php
namespace App\Services\Notifications;
use App\Models\Configuration;
use App\Models\Notification;
use App\Models\UserNotification;
class UserNotificationDispatchService
{
public function notifyUser(
int $userId,
string $title,
string $message,
array $channels = ['in_app'],
string $topic = 'general',
?string $targetGroup = null
): array {
if ($userId <= 0) {
return ['ok' => false, 'message' => 'Invalid user id.'];
}
$channels = array_values(array_unique(array_map('strtolower', (array) $channels)));
if (empty($channels)) {
$channels = ['in_app'];
}
$schoolYear = Configuration::getConfigValueByKey('school_year');
$semester = Configuration::getConfigValueByKey('semester');
$notification = Notification::query()->create([
'title' => $title,
'message' => $message,
'target_group' => $targetGroup ?: 'everyone',
'delivery_channels' => $channels,
'priority' => 'normal',
'status' => 'sent',
'scheduled_at' => now(),
'sent_at' => now(),
'school_year' => $schoolYear,
'semester' => $semester,
]);
UserNotification::query()->create([
'notification_id' => (int) $notification->id,
'user_id' => $userId,
'is_read' => 0,
'delivered' => in_array('in_app', $channels, true) ? 1 : 0,
'delivered_at' => in_array('in_app', $channels, true) ? now() : null,
'school_year' => $schoolYear,
'semester' => $semester,
]);
return [
'ok' => true,
'notification_id' => (int) $notification->id,
'topic' => $topic,
];
}
}