Commit 0b2c6122 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Mobile API Phase 2: Push notifications via Firebase Cloud Messaging

- Install kreait/firebase-php SDK
- Create PushNotificationService with 3-tier credential resolution
  (system_settings → storage file → env var, graceful degradation)
- Auto-deactivate invalid/unregistered FCM tokens on send failure
- Add Push case to NotificationChannel enum
- Create 6 event listeners for push notifications:
  - AttendanceMarked → notify guardians (present/absent)
  - PaymentReceived → confirm payment to guardians
  - InvoiceCreated → alert guardians of new invoice
  - SessionCancelled → notify all group participants
  - EnrollmentCreated → confirm enrollment to guardians
  - EvaluationShared → notify guardians of new evaluation
- Register all listeners in EventServiceProvider (queued)
- Create NotificationController API endpoints:
  - GET notifications (paginated + unread count)
  - PATCH notifications/{id}/read
  - POST notifications/read-all
  - GET/POST notifications/preferences
- Create scheduled push commands:
  - push:session-reminder (every minute, 30min before session)
  - push:installment-due (daily, 1 and 3 days before due)
- Add channel_push column to notification_preferences
- Total: 20 API endpoints now live
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 7a8a5594
<?php
namespace App\Console\Commands;
use App\Domain\Financial\Models\Installment;
use App\Domain\Notification\Services\PushNotificationService;
use App\Domain\Participant\Models\Participant;
use Illuminate\Console\Command;
class SendInstallmentDuePush extends Command
{
protected $signature = 'push:installment-due {--days=1 : Days before due date to send reminder}';
protected $description = 'Send push notification for upcoming installment due dates';
public function handle(PushNotificationService $pushService): int
{
$days = (int) $this->option('days');
$targetDate = now()->addDays($days)->toDateString();
$installments = Installment::where('due_date', $targetDate)
->where('status', 'pending')
->with(['paymentPlan.invoice'])
->get();
$count = 0;
foreach ($installments as $installment) {
$invoice = $installment->paymentPlan?->invoice;
if (!$invoice) {
continue;
}
if ($invoice->billable_type !== 'App\\Domain\\Participant\\Models\\Participant') {
continue;
}
$participant = Participant::find($invoice->billable_id);
if (!$participant) {
continue;
}
$amount = number_format($installment->amount / 100, 2);
$dueLabel = $days === 1 ? 'غداً' : "خلال {$days} أيام";
$pushService->sendToParticipantGuardians(
$participant,
'تذكير بالقسط',
"قسط بقيمة {$amount} ج.م مستحق {$dueLabel}",
[
'type' => 'installment_due',
'participant_uuid' => $participant->uuid,
'invoice_uuid' => $invoice->uuid,
'amount' => $installment->amount,
'due_date' => $targetDate,
],
'installment.due_reminder'
);
$count++;
}
$this->info("Sent installment reminders for {$count} installments.");
return self::SUCCESS;
}
}
<?php
namespace App\Console\Commands;
use App\Domain\Notification\Services\PushNotificationService;
use App\Domain\Training\Models\TrainingSession;
use Illuminate\Console\Command;
class SendSessionReminderPush extends Command
{
protected $signature = 'push:session-reminder {--minutes=30 : Minutes before session to send reminder}';
protected $description = 'Send push notification to participants about upcoming session';
public function handle(PushNotificationService $pushService): int
{
$minutes = (int) $this->option('minutes');
$targetTime = now()->addMinutes($minutes);
$sessions = TrainingSession::where('session_date', now()->toDateString())
->where('start_time', '>=', $targetTime->format('H:i:00'))
->where('start_time', '<=', $targetTime->addMinute()->format('H:i:00'))
->where('status', 'scheduled')
->with('group')
->get();
$count = 0;
foreach ($sessions as $session) {
$group = $session->group;
if (!$group) {
continue;
}
$groupName = $group->name_ar ?? 'المجموعة';
$time = $session->start_time;
$pushService->sendToGroupParticipants(
$group,
'تذكير بالجلسة',
"جلسة {$groupName} تبدأ خلال {$minutes} دقيقة ({$time})",
[
'type' => 'session_reminder',
'session_id' => $session->id,
'group_id' => $group->id,
'start_time' => $time,
],
'session.reminder'
);
$count++;
}
$this->info("Sent reminders for {$count} sessions.");
return self::SUCCESS;
}
}
......@@ -8,6 +8,7 @@
case Email = 'email';
case Sms = 'sms';
case Whatsapp = 'whatsapp';
case Push = 'push';
public function label(): string
{
......@@ -16,6 +17,7 @@ public function label(): string
self::Email => 'بريد إلكتروني',
self::Sms => 'رسالة نصية',
self::Whatsapp => 'واتساب',
self::Push => 'إشعار الهاتف',
};
}
}
<?php
namespace App\Domain\Notification\Listeners;
use App\Domain\Attendance\Events\AttendanceMarked;
use App\Domain\Notification\Services\PushNotificationService;
use App\Domain\Participant\Models\Participant;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendAttendancePush implements ShouldQueue
{
public function __construct(private PushNotificationService $pushService) {}
public function handle(AttendanceMarked $event): void
{
$record = $event->record;
if ($record->subject_type !== 'App\\Domain\\Participant\\Models\\Participant') {
return;
}
$participant = Participant::with('person')->find($record->subject_id);
if (!$participant) {
return;
}
$name = $participant->person?->name_ar ?? 'المشترك';
$status = $record->status->value ?? $record->status;
if (in_array($status, ['present', 'late'])) {
$title = 'تسجيل حضور ✓';
$body = "تم تسجيل حضور {$name} في جلسة اليوم";
} elseif (in_array($status, ['absent', 'no_show'])) {
$title = 'تسجيل غياب';
$body = "تم تسجيل غياب {$name} في جلسة اليوم";
} else {
return;
}
$this->pushService->sendToParticipantGuardians(
$participant,
$title,
$body,
['type' => 'attendance', 'participant_uuid' => $participant->uuid, 'status' => $status],
'attendance.marked'
);
}
}
<?php
namespace App\Domain\Notification\Listeners;
use App\Domain\Notification\Services\PushNotificationService;
use App\Domain\Participant\Models\Participant;
use App\Domain\Training\Events\EnrollmentCreated;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendEnrollmentCreatedPush implements ShouldQueue
{
public function __construct(private PushNotificationService $pushService) {}
public function handle(EnrollmentCreated $event): void
{
$enrollment = $event->enrollment;
$participant = Participant::with('person')->find($enrollment->participant_id);
if (!$participant) {
return;
}
$name = $participant->person?->name_ar ?? 'المشترك';
$group = $enrollment->group;
$programName = $group?->program?->name_ar ?? 'البرنامج';
$this->pushService->sendToParticipantGuardians(
$participant,
'تأكيد الاشتراك',
"تم تسجيل {$name} في {$programName}",
[
'type' => 'enrollment',
'participant_uuid' => $participant->uuid,
'enrollment_id' => $enrollment->id,
],
'enrollment.created'
);
}
}
<?php
namespace App\Domain\Notification\Listeners;
use App\Domain\Notification\Services\PushNotificationService;
use App\Domain\Participant\Models\Participant;
use App\Domain\Training\Events\EvaluationShared;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendEvaluationSharedPush implements ShouldQueue
{
public function __construct(private PushNotificationService $pushService) {}
public function handle(EvaluationShared $event): void
{
$evaluation = $event->evaluation;
$participant = Participant::with('person')->find($evaluation->participant_id);
if (!$participant) {
return;
}
$name = $participant->person?->name_ar ?? 'المشترك';
$this->pushService->sendToParticipantGuardians(
$participant,
'تقييم جديد',
"تم مشاركة تقييم جديد لـ {$name}",
[
'type' => 'evaluation',
'participant_uuid' => $participant->uuid,
'evaluation_id' => $evaluation->id,
],
'evaluation.shared'
);
}
}
<?php
namespace App\Domain\Notification\Listeners;
use App\Domain\Financial\Events\InvoiceCreated;
use App\Domain\Notification\Services\PushNotificationService;
use App\Domain\Participant\Models\Participant;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendInvoiceCreatedPush implements ShouldQueue
{
public function __construct(private PushNotificationService $pushService) {}
public function handle(InvoiceCreated $event): void
{
$invoice = $event->invoice;
if ($invoice->billable_type !== 'App\\Domain\\Participant\\Models\\Participant') {
return;
}
$participant = Participant::find($invoice->billable_id);
if (!$participant) {
return;
}
$amount = number_format($invoice->total_amount / 100, 2);
$this->pushService->sendToParticipantGuardians(
$participant,
'فاتورة جديدة',
"فاتورة بقيمة {$amount} ج.م بانتظار الدفع",
[
'type' => 'invoice',
'participant_uuid' => $participant->uuid,
'invoice_uuid' => $invoice->uuid,
'amount' => $invoice->total_amount,
],
'invoice.created'
);
}
}
<?php
namespace App\Domain\Notification\Listeners;
use App\Domain\Financial\Events\PaymentReceived;
use App\Domain\Notification\Services\PushNotificationService;
use App\Domain\Participant\Models\Participant;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendPaymentPush implements ShouldQueue
{
public function __construct(private PushNotificationService $pushService) {}
public function handle(PaymentReceived $event): void
{
$payment = $event->payment;
$invoice = $payment->invoice;
if (!$invoice) {
return;
}
$amount = number_format($payment->amount / 100, 2);
if ($invoice->billable_type === 'App\\Domain\\Participant\\Models\\Participant') {
$participant = Participant::find($invoice->billable_id);
if (!$participant) {
return;
}
$this->pushService->sendToParticipantGuardians(
$participant,
'تأكيد الدفع',
"تم تأكيد دفع {$amount} ج.م بنجاح",
[
'type' => 'payment',
'participant_uuid' => $participant->uuid,
'invoice_uuid' => $invoice->uuid,
'amount' => $payment->amount,
],
'payment.confirmed'
);
}
}
}
<?php
namespace App\Domain\Notification\Listeners;
use App\Domain\Notification\Services\PushNotificationService;
use App\Domain\Training\Events\SessionCancelled;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendSessionCancelledPush implements ShouldQueue
{
public function __construct(private PushNotificationService $pushService) {}
public function handle(SessionCancelled $event): void
{
$session = $event->session;
$group = $session->group;
if (!$group) {
return;
}
$groupName = $group->name_ar ?? 'المجموعة';
$date = $session->session_date?->format('Y-m-d') ?? '';
$this->pushService->sendToGroupParticipants(
$group,
'إلغاء جلسة',
"تم إلغاء جلسة {$groupName} يوم {$date}",
[
'type' => 'session_cancelled',
'session_id' => $session->id,
'group_id' => $group->id,
'date' => $date,
],
'session.cancelled'
);
}
}
......@@ -15,6 +15,7 @@ class NotificationPreference extends Model
'channel_email',
'channel_sms',
'channel_in_app',
'channel_push',
'digest_mode',
'digest_time',
];
......@@ -23,6 +24,7 @@ class NotificationPreference extends Model
'channel_email' => 'boolean',
'channel_sms' => 'boolean',
'channel_in_app' => 'boolean',
'channel_push' => 'boolean',
'digest_mode' => 'boolean',
];
......
<?php
namespace App\Domain\Notification\Services;
use App\Domain\Identity\Models\Guardian;
use App\Domain\Notification\Enums\NotificationChannel;
use App\Domain\Notification\Enums\NotificationStatus;
use App\Domain\Notification\Models\NotificationLog;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Models\DeviceToken;
use App\Domain\Shared\Models\SystemSetting;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Kreait\Firebase\Factory;
use Kreait\Firebase\Messaging\CloudMessage;
use Kreait\Firebase\Messaging\Notification;
class PushNotificationService
{
private ?\Kreait\Firebase\Contract\Messaging $messaging = null;
private bool $initialized = false;
public function sendToUser(User $user, string $title, string $body, array $data = [], ?string $eventType = null): void
{
$tokens = DeviceToken::withoutGlobalScope('academy')
->where('user_id', $user->id)
->where('is_active', true)
->pluck('device_token')
->toArray();
if (empty($tokens)) {
return;
}
$this->sendToTokens($tokens, $title, $body, $data, $user->academy_id, $user->id, $eventType);
}
public function sendToParticipantGuardians(Participant $participant, string $title, string $body, array $data = [], ?string $eventType = null): void
{
$userIds = $this->getGuardianUserIds($participant);
if ($userIds->isEmpty()) {
return;
}
$tokens = DeviceToken::withoutGlobalScope('academy')
->whereIn('user_id', $userIds)
->where('is_active', true)
->pluck('device_token')
->toArray();
if (empty($tokens)) {
return;
}
$this->sendToTokens($tokens, $title, $body, $data, $participant->academy_id, $userIds->first(), $eventType);
}
public function sendToUsers(Collection $userIds, string $title, string $body, array $data = [], ?int $academyId = null, ?string $eventType = null): void
{
$tokens = DeviceToken::withoutGlobalScope('academy')
->whereIn('user_id', $userIds)
->where('is_active', true)
->pluck('device_token')
->toArray();
if (empty($tokens)) {
return;
}
$this->sendToTokens($tokens, $title, $body, $data, $academyId, $userIds->first(), $eventType);
}
public function sendToGroupParticipants(\App\Domain\Training\Models\TrainingGroup $group, string $title, string $body, array $data = [], ?string $eventType = null): void
{
$participantIds = $group->enrollments()
->where('status', 'active')
->pluck('participant_id');
$participants = Participant::whereIn('id', $participantIds)->get();
$allUserIds = collect();
foreach ($participants as $participant) {
$allUserIds = $allUserIds->merge($this->getGuardianUserIds($participant));
}
$allUserIds = $allUserIds->unique();
if ($allUserIds->isEmpty()) {
return;
}
$tokens = DeviceToken::withoutGlobalScope('academy')
->whereIn('user_id', $allUserIds)
->where('is_active', true)
->pluck('device_token')
->toArray();
if (empty($tokens)) {
return;
}
$this->sendToTokens($tokens, $title, $body, $data, $group->academy_id, null, $eventType);
}
private function sendToTokens(array $tokens, string $title, string $body, array $data, ?int $academyId, ?int $recipientUserId, ?string $eventType): void
{
$messaging = $this->getMessaging();
if (!$messaging) {
Log::info('[Push] Firebase not configured — skipping push notification', [
'title' => $title,
'tokens_count' => count($tokens),
]);
return;
}
$notification = Notification::create($title, $body);
$message = CloudMessage::new()
->withNotification($notification)
->withData(array_merge($data, ['click_action' => 'FLUTTER_NOTIFICATION_CLICK']));
try {
$report = $messaging->sendMulticast($message, $tokens);
$successCount = $report->successes()->count();
$failureCount = $report->failures()->count();
// Deactivate invalid tokens
if ($failureCount > 0) {
$invalidTokens = [];
foreach ($report->failures()->getItems() as $failure) {
$error = $failure->error();
if ($error && in_array($error->value, ['UNREGISTERED', 'INVALID_ARGUMENT'])) {
$invalidTokens[] = $failure->target()->value();
}
}
if (!empty($invalidTokens)) {
DeviceToken::withoutGlobalScope('academy')
->whereIn('device_token', $invalidTokens)
->update(['is_active' => false]);
}
}
// Log the notification
if ($academyId && $recipientUserId) {
NotificationLog::create([
'academy_id' => $academyId,
'recipient_type' => 'user',
'recipient_id' => $recipientUserId,
'event_type' => $eventType ?? 'push_notification',
'channel' => NotificationChannel::Push->value,
'subject' => $title,
'body' => $body,
'status' => $successCount > 0 ? NotificationStatus::Sent->value : NotificationStatus::Failed->value,
'error_message' => $failureCount > 0 ? "{$failureCount} failed out of " . count($tokens) : null,
'sent_at' => $successCount > 0 ? now() : null,
'metadata' => [
'tokens_sent' => count($tokens),
'success_count' => $successCount,
'failure_count' => $failureCount,
'data' => $data,
],
]);
}
Log::info('[Push] Sent', [
'title' => $title,
'success' => $successCount,
'failed' => $failureCount,
]);
} catch (\Throwable $e) {
Log::error('[Push] Failed to send', [
'title' => $title,
'error' => $e->getMessage(),
]);
if ($academyId && $recipientUserId) {
NotificationLog::create([
'academy_id' => $academyId,
'recipient_type' => 'user',
'recipient_id' => $recipientUserId,
'event_type' => $eventType ?? 'push_notification',
'channel' => NotificationChannel::Push->value,
'subject' => $title,
'body' => $body,
'status' => NotificationStatus::Failed->value,
'error_message' => $e->getMessage(),
'metadata' => ['data' => $data],
]);
}
}
}
private function getGuardianUserIds(Participant $participant): Collection
{
$guardians = $participant->guardians()->with('user')->get();
return $guardians
->map(fn ($g) => $g->user?->id)
->filter()
->values();
}
private function getMessaging(): ?\Kreait\Firebase\Contract\Messaging
{
if ($this->initialized) {
return $this->messaging;
}
$this->initialized = true;
try {
$credentials = $this->resolveCredentials();
if (!$credentials) {
return null;
}
$factory = (new Factory)->withServiceAccount($credentials);
$this->messaging = $factory->createMessaging();
return $this->messaging;
} catch (\Throwable $e) {
Log::warning('[Push] Failed to initialize Firebase', ['error' => $e->getMessage()]);
return null;
}
}
private function resolveCredentials(): ?array
{
// 1. System settings (per-academy JSON)
$json = SystemSetting::get('firebase_service_account_json');
if ($json) {
$decoded = is_string($json) ? json_decode($json, true) : $json;
if (is_array($decoded) && isset($decoded['project_id'])) {
return $decoded;
}
}
// 2. File in storage
$filePath = storage_path('app/firebase/service-account.json');
if (file_exists($filePath)) {
$decoded = json_decode(file_get_contents($filePath), true);
if (is_array($decoded) && isset($decoded['project_id'])) {
return $decoded;
}
}
// 3. Environment variable (path to file)
$envPath = env('FIREBASE_CREDENTIALS');
if ($envPath && file_exists($envPath)) {
$decoded = json_decode(file_get_contents($envPath), true);
if (is_array($decoded) && isset($decoded['project_id'])) {
return $decoded;
}
}
return null;
}
}
<?php
namespace App\Http\Controllers\Api\V1;
use App\Domain\Notification\Enums\NotificationChannel;
use App\Domain\Notification\Enums\NotificationStatus;
use App\Domain\Notification\Models\NotificationLog;
use App\Domain\Notification\Models\NotificationPreference;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class NotificationController extends Controller
{
public function index(Request $request): JsonResponse
{
$user = $request->user();
$notifications = NotificationLog::where('recipient_type', 'user')
->where('recipient_id', $user->id)
->whereIn('channel', [
NotificationChannel::InApp->value,
NotificationChannel::Push->value,
])
->orderByDesc('created_at')
->paginate(20);
return response()->json([
'data' => $notifications->map(fn ($n) => [
'id' => $n->id,
'event_type' => $n->event_type,
'title' => $n->subject,
'body' => $n->body,
'is_read' => $n->read_at !== null,
'read_at' => $n->read_at?->toIso8601String(),
'sent_at' => $n->sent_at?->toIso8601String(),
'created_at' => $n->created_at?->toIso8601String(),
'metadata' => $n->metadata,
]),
'unread_count' => NotificationLog::where('recipient_type', 'user')
->where('recipient_id', $user->id)
->whereIn('channel', [
NotificationChannel::InApp->value,
NotificationChannel::Push->value,
])
->where('status', NotificationStatus::Sent)
->whereNull('read_at')
->count(),
'meta' => [
'current_page' => $notifications->currentPage(),
'last_page' => $notifications->lastPage(),
'per_page' => $notifications->perPage(),
'total' => $notifications->total(),
],
]);
}
public function markAsRead(int $id, Request $request): JsonResponse
{
$user = $request->user();
$notification = NotificationLog::where('id', $id)
->where('recipient_type', 'user')
->where('recipient_id', $user->id)
->firstOrFail();
if (!$notification->read_at) {
$notification->update(['read_at' => now()]);
}
return response()->json(['success' => true]);
}
public function markAllAsRead(Request $request): JsonResponse
{
$user = $request->user();
NotificationLog::where('recipient_type', 'user')
->where('recipient_id', $user->id)
->whereIn('channel', [
NotificationChannel::InApp->value,
NotificationChannel::Push->value,
])
->whereNull('read_at')
->update(['read_at' => now()]);
return response()->json(['success' => true]);
}
public function getPreferences(Request $request): JsonResponse
{
$user = $request->user();
$prefs = NotificationPreference::where('user_id', $user->id)->get();
return response()->json([
'data' => $prefs->map(fn ($p) => [
'event_type' => $p->event_type,
'channel_email' => (bool) $p->channel_email,
'channel_sms' => (bool) $p->channel_sms,
'channel_push' => (bool) ($p->channel_push ?? true),
'digest_mode' => (bool) $p->digest_mode,
]),
]);
}
public function updatePreferences(Request $request): JsonResponse
{
$request->validate([
'preferences' => 'required|array',
'preferences.*.event_type' => 'required|string',
'preferences.*.channel_push' => 'boolean',
'preferences.*.channel_email' => 'boolean',
'preferences.*.channel_sms' => 'boolean',
]);
$user = $request->user();
foreach ($request->preferences as $pref) {
NotificationPreference::updateOrCreate(
['user_id' => $user->id, 'event_type' => $pref['event_type']],
[
'channel_email' => $pref['channel_email'] ?? true,
'channel_sms' => $pref['channel_sms'] ?? true,
'channel_push' => $pref['channel_push'] ?? true,
]
);
}
return response()->json(['success' => true]);
}
}
......@@ -10,6 +10,7 @@ class EventServiceProvider extends ServiceProvider
// Financial Events
\App\Domain\Financial\Events\InvoiceCreated::class => [
\App\Domain\Financial\Listeners\SendInvoiceNotification::class,
\App\Domain\Notification\Listeners\SendInvoiceCreatedPush::class,
],
\App\Domain\Financial\Events\InvoicePaid::class => [
\App\Domain\Financial\Listeners\ActivateEnrollmentOnPayment::class,
......@@ -30,6 +31,7 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Financial\Listeners\SendPaymentConfirmation::class,
\App\Domain\Financial\Listeners\SendPaymentNotification::class,
\App\Domain\Financial\Listeners\UpdateCashSessionTotals::class,
\App\Domain\Notification\Listeners\SendPaymentPush::class,
],
\App\Domain\Financial\Events\PaymentPlanDefaulted::class => [
\App\Domain\Financial\Listeners\SendPaymentPlanDefaultedNotification::class,
......@@ -48,6 +50,7 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Training\Events\EnrollmentCreated::class => [
\App\Domain\Training\Listeners\UpdateGroupCount::class,
\App\Domain\Training\Listeners\GenerateAttendanceRecords::class,
\App\Domain\Notification\Listeners\SendEnrollmentCreatedPush::class,
],
\App\Domain\Training\Events\EnrollmentCancelled::class => [
\App\Domain\Training\Listeners\UpdateGroupCount::class,
......@@ -67,6 +70,7 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Training\Listeners\NotifySessionCancellation::class,
\App\Domain\Training\Listeners\CancelLinkedReservation::class,
\App\Domain\HR\Listeners\HandleSessionCancelled::class,
\App\Domain\Notification\Listeners\SendSessionCancelledPush::class,
],
\App\Domain\Training\Events\SessionCompleted::class => [
\App\Domain\Training\Listeners\SendSessionCompletedNotification::class,
......@@ -82,11 +86,13 @@ class EventServiceProvider extends ServiceProvider
],
\App\Domain\Training\Events\EvaluationShared::class => [
\App\Domain\Training\Listeners\SendEvaluationSharedNotification::class,
\App\Domain\Notification\Listeners\SendEvaluationSharedPush::class,
],
// Attendance Events
\App\Domain\Attendance\Events\AttendanceMarked::class => [
\App\Domain\HR\Listeners\GenerateTrainerCompensation::class,
\App\Domain\Notification\Listeners\SendAttendancePush::class,
],
\App\Domain\Attendance\Events\ParticipantAbsent::class => [
\App\Domain\Attendance\Listeners\NotifyGuardianOfAbsence::class,
......
......@@ -7,6 +7,7 @@
"license": "MIT",
"require": {
"php": "^8.4",
"kreait/firebase-php": "^8.3",
"laravel/framework": "^13.8",
"laravel/sanctum": "^4.3",
"laravel/tinker": "^3.0",
......
......@@ -4,8 +4,203 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "510aa304988e6cd8b1d427dd945253d5",
"content-hash": "a5ec0e7c46f05c066ea4522fcd0be41e",
"packages": [
{
"name": "beste/clock",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/beste/clock.git",
"reference": "7004b55fcd54737b539886244b3a3b2188181974"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/beste/clock/zipball/7004b55fcd54737b539886244b3a3b2188181974",
"reference": "7004b55fcd54737b539886244b3a3b2188181974",
"shasum": ""
},
"require": {
"php": "^8.0",
"psr/clock": "^1.0"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"require-dev": {
"phpstan/extension-installer": "^1.2",
"phpstan/phpstan": "^1.9.1",
"phpstan/phpstan-phpunit": "^1.2.2",
"phpstan/phpstan-strict-rules": "^1.4.4",
"phpunit/phpunit": "^9.5.26",
"psalm/plugin-phpunit": "^0.16.1",
"vimeo/psalm": "^4.29"
},
"type": "library",
"autoload": {
"files": [
"src/Clock.php"
],
"psr-4": {
"Beste\\Clock\\": "src/Clock"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jérôme Gamez",
"email": "jerome@gamez.name"
}
],
"description": "A collection of Clock implementations",
"keywords": [
"clock",
"clock-interface",
"psr-20",
"psr20"
],
"support": {
"issues": "https://github.com/beste/clock/issues",
"source": "https://github.com/beste/clock/tree/3.0.0"
},
"funding": [
{
"url": "https://github.com/jeromegamez",
"type": "github"
}
],
"time": "2022-11-26T18:03:05+00:00"
},
{
"name": "beste/in-memory-cache",
"version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/beste/in-memory-cache-php.git",
"reference": "d6991967b05e820cb1bcd2d31ca11ea8b1ca3f77"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/beste/in-memory-cache-php/zipball/d6991967b05e820cb1bcd2d31ca11ea8b1ca3f77",
"reference": "d6991967b05e820cb1bcd2d31ca11ea8b1ca3f77",
"shasum": ""
},
"require": {
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
"psr/cache": "^2.0 || ^3.0",
"psr/clock": "^1.0"
},
"provide": {
"psr/cache-implementation": "2.0 || 3.0"
},
"require-dev": {
"beste/clock": "^3.0",
"friendsofphp/php-cs-fixer": "^3.95.1",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.1.51",
"phpstan/phpstan-deprecation-rules": "^2.0.4",
"phpstan/phpstan-phpunit": "^2.0.16",
"phpstan/phpstan-strict-rules": "^2.0.10",
"phpunit/phpunit": "^12.5.23",
"symfony/var-dumper": "^7.4.8 || ^v8.0.8"
},
"suggest": {
"psr/clock-implementation": "Allows injecting a Clock, for example a frozen clock for testing"
},
"type": "library",
"autoload": {
"psr-4": {
"Beste\\Cache\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jérôme Gamez",
"email": "jerome@gamez.name"
}
],
"description": "A PSR-6 In-Memory cache that can be used as a fallback implementation and/or in tests.",
"keywords": [
"beste",
"cache",
"psr-6"
],
"support": {
"issues": "https://github.com/beste/in-memory-cache-php/issues",
"source": "https://github.com/beste/in-memory-cache-php/tree/1.5.0"
},
"time": "2026-04-27T12:29:41+00:00"
},
{
"name": "beste/json",
"version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/beste/json.git",
"reference": "976525f1ce2323a4e044364269d60b402603e216"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/beste/json/zipball/976525f1ce2323a4e044364269d60b402603e216",
"reference": "976525f1ce2323a4e044364269d60b402603e216",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
},
"require-dev": {
"phpstan/extension-installer": "^1.3",
"phpstan/phpstan": "^2.0.4",
"phpstan/phpstan-phpunit": "^2.0.2",
"phpstan/phpstan-strict-rules": "^2.0.1",
"phpunit/phpunit": "^10.4.2",
"rector/rector": "^2.0.3"
},
"type": "library",
"autoload": {
"files": [
"src/Json.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jérôme Gamez",
"email": "jerome@gamez.name"
}
],
"description": "A simple JSON helper to decode and encode JSON",
"keywords": [
"helper",
"json"
],
"support": {
"issues": "https://github.com/beste/json/issues",
"source": "https://github.com/beste/json/tree/1.7.0"
},
"funding": [
{
"url": "https://github.com/jeromegamez",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/beste/json",
"type": "tidelift"
}
],
"time": "2025-09-11T23:36:19+00:00"
},
{
"name": "brick/math",
"version": "0.18.0",
......@@ -134,6 +329,83 @@
],
"time": "2024-02-09T16:56:22+00:00"
},
{
"name": "cuyz/valinor",
"version": "2.5.0",
"source": {
"type": "git",
"url": "https://github.com/CuyZ/Valinor.git",
"reference": "afbe7352692d5925a76edd53d5998161334056d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/CuyZ/Valinor/zipball/afbe7352692d5925a76edd53d5998161334056d4",
"reference": "afbe7352692d5925a76edd53d5998161334056d4",
"shasum": ""
},
"require": {
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
},
"conflict": {
"phpstan/phpstan": "<1.0 || >= 3.0",
"vimeo/psalm": "<5.0 || >=7.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.91",
"infection/infection": "^0.32",
"marcocesarato/php-conventional-changelog": "^1.12",
"mikey179/vfsstream": "^1.6.10",
"phpbench/phpbench": "^1.3",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^11.5",
"psr/http-message": "^2.0",
"rector/rector": "^2.0",
"vimeo/psalm": "^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"CuyZ\\Valinor\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Romain Canon",
"email": "romain.hydrocanon@gmail.com",
"homepage": "https://github.com/romm"
}
],
"description": "Dependency free PHP library that helps to map any input into a strongly-typed structure.",
"homepage": "https://github.com/CuyZ/Valinor",
"keywords": [
"array",
"conversion",
"hydrator",
"json",
"mapper",
"mapping",
"object",
"tree",
"yaml"
],
"support": {
"issues": "https://github.com/CuyZ/Valinor/issues",
"source": "https://github.com/CuyZ/Valinor/tree/2.5.0"
},
"funding": [
{
"url": "https://github.com/romm",
"type": "github"
}
],
"time": "2026-06-28T21:53:40+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
......@@ -507,6 +779,128 @@
],
"time": "2025-03-06T22:45:56+00:00"
},
{
"name": "fig/http-message-util",
"version": "1.1.5",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message-util.git",
"reference": "9d94dc0154230ac39e5bf89398b324a86f63f765"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765",
"reference": "9d94dc0154230ac39e5bf89398b324a86f63f765",
"shasum": ""
},
"require": {
"php": "^5.3 || ^7.0 || ^8.0"
},
"suggest": {
"psr/http-message": "The package containing the PSR-7 interfaces"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Fig\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Utility classes and constants for use with PSR-7 (psr/http-message)",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"issues": "https://github.com/php-fig/http-message-util/issues",
"source": "https://github.com/php-fig/http-message-util/tree/1.1.5"
},
"time": "2020-11-24T22:02:12+00:00"
},
{
"name": "firebase/php-jwt",
"version": "v7.1.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/php-jwt.git",
"reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/php-jwt/zipball/b374a5d1a4f1f67fadc2165cdb284645945e2fc0",
"reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpfastcache/phpfastcache": "^9.2",
"phpseclib/phpseclib": "~3.0",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"suggest": {
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present",
"phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures"
},
"type": "library",
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Neuman Vong",
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"email": "anant@php.net",
"role": "Developer"
}
],
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
"homepage": "https://github.com/googleapis/php-jwt",
"keywords": [
"jwt",
"php"
],
"support": {
"issues": "https://github.com/googleapis/php-jwt/issues",
"source": "https://github.com/googleapis/php-jwt/tree/v7.1.0"
},
"time": "2026-06-11T17:54:14+00:00"
},
{
"name": "fruitcake/php-cors",
"version": "v1.4.0",
......@@ -579,25 +973,469 @@
"time": "2025-12-03T09:33:47+00:00"
},
{
"name": "graham-campbell/result-type",
"version": "v1.1.4",
"name": "google/auth",
"version": "v1.53.0",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b"
"url": "https://github.com/googleapis/google-auth-library-php.git",
"reference": "d677d0b0c4bd52ab222a85df8e74e0d491c0cc5a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b",
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/d677d0b0c4bd52ab222a85df8e74e0d491c0cc5a",
"reference": "d677d0b0c4bd52ab222a85df8e74e0d491c0cc5a",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.5"
"firebase/php-jwt": "^6.0||^7.0",
"guzzlehttp/guzzle": "^7.8.2||^8.0",
"guzzlehttp/psr7": "^2.6.3||^3.0",
"php": "^8.1",
"psr/cache": "^2.0||^3.0",
"psr/http-client": "^1.0",
"psr/http-message": "^1.1||^2.0",
"psr/log": "^2.0||^3.0"
},
"require-dev": {
"phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7"
"guzzlehttp/promises": "^2.0.3||^3.0",
"kelvinmo/simplejwt": "^1.1.0",
"phpseclib/phpseclib": "^3.0.35",
"phpspec/prophecy-phpunit": "^2.1",
"phpunit/phpunit": "^9.6",
"sebastian/comparator": ">=1.2.3",
"squizlabs/php_codesniffer": "^4.0",
"symfony/filesystem": "^6.3||^7.3",
"symfony/process": "^6.0||^7.0",
"webmozart/assert": "^1.11||^2.0"
},
"suggest": {
"phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2."
},
"type": "library",
"autoload": {
"psr-4": {
"Google\\Auth\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Google Auth Library for PHP",
"homepage": "https://github.com/google/google-auth-library-php",
"keywords": [
"Authentication",
"google",
"oauth2"
],
"support": {
"docs": "https://cloud.google.com/php/docs/reference/auth/latest",
"issues": "https://github.com/googleapis/google-auth-library-php/issues",
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.53.0"
},
"time": "2026-07-22T22:36:10+00:00"
},
{
"name": "google/cloud-core",
"version": "v1.72.4",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-cloud-php-core.git",
"reference": "d46317d5a9e779189513f89bfc3e4e9364b5eaf2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-cloud-php-core/zipball/d46317d5a9e779189513f89bfc3e4e9364b5eaf2",
"reference": "d46317d5a9e779189513f89bfc3e4e9364b5eaf2",
"shasum": ""
},
"require": {
"google/auth": "^1.34",
"google/gax": "^1.38.0",
"guzzlehttp/guzzle": "^6.5.8||^7.4.4",
"guzzlehttp/promises": "^1.4||^2.0",
"guzzlehttp/psr7": "^2.6",
"monolog/monolog": "^2.9||^3.0",
"php": "^8.1",
"psr/http-message": "^1.0||^2.0",
"rize/uri-template": "~0.3||~0.4"
},
"require-dev": {
"erusev/parsedown": "^1.6",
"google/cloud-common-protos": "~0.5||^1.0",
"nikic/php-parser": "^5.6",
"opis/closure": "^3.7|^4.0",
"phpdocumentor/reflection": "^6.0",
"phpdocumentor/reflection-docblock": "^5.3.3||^6.0",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "2.*"
},
"suggest": {
"opis/closure": "May be used to serialize closures to process jobs in the batch daemon. Please require version ^3.",
"symfony/lock": "Required for the Spanner cached based session pool. Please require the following commit: 3.3.x-dev#1ba6ac9"
},
"bin": [
"bin/google-cloud-batch"
],
"type": "library",
"extra": {
"component": {
"id": "cloud-core",
"path": "Core",
"entry": "src/ServiceBuilder.php",
"target": "googleapis/google-cloud-php-core.git"
}
},
"autoload": {
"psr-4": {
"Google\\Cloud\\Core\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Google Cloud PHP shared dependency, providing functionality useful to all components.",
"support": {
"source": "https://github.com/googleapis/google-cloud-php-core/tree/v1.72.4"
},
"time": "2026-07-07T16:28:46+00:00"
},
{
"name": "google/cloud-storage",
"version": "v2.4.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-cloud-php-storage.git",
"reference": "f1254a02dc9b319d1f58723b1adcecdc9eb376ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-cloud-php-storage/zipball/f1254a02dc9b319d1f58723b1adcecdc9eb376ad",
"reference": "f1254a02dc9b319d1f58723b1adcecdc9eb376ad",
"shasum": ""
},
"require": {
"google/cloud-core": "^1.72.0",
"php": "^8.1",
"ramsey/uuid": "^4.2.3"
},
"require-dev": {
"erusev/parsedown": "^1.6",
"google/cloud-pubsub": "^2.0",
"nikic/php-parser": "^5",
"phpdocumentor/reflection": "^6.0",
"phpdocumentor/reflection-docblock": "^5.3.3",
"phpseclib/phpseclib": "^2.0||^3.0",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "2.*"
},
"suggest": {
"google/cloud-pubsub": "May be used to register a topic to receive bucket notifications.",
"phpseclib/phpseclib": "May be used in place of OpenSSL for creating signed Cloud Storage URLs. Please require version ^2."
},
"type": "library",
"extra": {
"component": {
"id": "cloud-storage",
"path": "Storage",
"entry": "src/StorageClient.php",
"target": "googleapis/google-cloud-php-storage.git"
}
},
"autoload": {
"psr-4": {
"Google\\Cloud\\Storage\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Cloud Storage Client for PHP",
"support": {
"source": "https://github.com/googleapis/google-cloud-php-storage/tree/v2.4.0"
},
"time": "2026-07-07T16:28:46+00:00"
},
{
"name": "google/common-protos",
"version": "4.14.1",
"source": {
"type": "git",
"url": "https://github.com/googleapis/common-protos-php.git",
"reference": "4eb6813b8068653e055fc8a63dbda3446f3e8869"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/common-protos-php/zipball/4eb6813b8068653e055fc8a63dbda3446f3e8869",
"reference": "4eb6813b8068653e055fc8a63dbda3446f3e8869",
"shasum": ""
},
"require": {
"google/protobuf": "^4.31||^5.0",
"php": "^8.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6"
},
"type": "library",
"extra": {
"component": {
"id": "common-protos",
"path": "CommonProtos",
"entry": "README.md",
"target": "googleapis/common-protos-php.git"
}
},
"autoload": {
"psr-4": {
"Google\\Api\\": "src/Api",
"Google\\Iam\\": "src/Iam",
"Google\\Rpc\\": "src/Rpc",
"Google\\Type\\": "src/Type",
"Google\\Cloud\\": "src/Cloud",
"GPBMetadata\\Google\\Api\\": "metadata/Api",
"GPBMetadata\\Google\\Iam\\": "metadata/Iam",
"GPBMetadata\\Google\\Rpc\\": "metadata/Rpc",
"GPBMetadata\\Google\\Type\\": "metadata/Type",
"GPBMetadata\\Google\\Cloud\\": "metadata/Cloud",
"GPBMetadata\\Google\\Logging\\": "metadata/Logging"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Google API Common Protos for PHP",
"homepage": "https://github.com/googleapis/common-protos-php",
"keywords": [
"google"
],
"support": {
"source": "https://github.com/googleapis/common-protos-php/tree/v4.14.1"
},
"time": "2026-06-17T23:07:32+00:00"
},
{
"name": "google/gax",
"version": "v1.46.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/gax-php.git",
"reference": "32824ff2b65fd4fe6d916f667cdc31445579e9f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/gax-php/zipball/32824ff2b65fd4fe6d916f667cdc31445579e9f2",
"reference": "32824ff2b65fd4fe6d916f667cdc31445579e9f2",
"shasum": ""
},
"require": {
"google/auth": "^1.52",
"google/common-protos": "^4.9",
"google/grpc-gcp": "^0.4",
"google/longrunning": "~0.4",
"google/protobuf": "^4.31||^5.34",
"grpc/grpc": "^1.13",
"guzzlehttp/promises": "^2.0",
"guzzlehttp/psr7": "^2.0",
"php": "^8.1",
"ramsey/uuid": "^4.0"
},
"conflict": {
"ext-protobuf": "<4.31.0"
},
"require-dev": {
"google/cloud-tools": "^0.16.1",
"phpspec/prophecy-phpunit": "^2.1",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^9.6"
},
"type": "library",
"extra": {
"component": {
"id": "gax",
"path": "Gax",
"entry": "README.md",
"target": "googleapis/gax-php.git"
}
},
"autoload": {
"psr-4": {
"Google\\ApiCore\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Google API Core for PHP",
"homepage": "https://github.com/googleapis/gax-php",
"keywords": [
"google"
],
"support": {
"issues": "https://github.com/googleapis/gax-php/issues",
"source": "https://github.com/googleapis/gax-php/tree/v1.46.0"
},
"time": "2026-07-22T05:23:19+00:00"
},
{
"name": "google/grpc-gcp",
"version": "0.4.2",
"source": {
"type": "git",
"url": "https://github.com/GoogleCloudPlatform/grpc-gcp-php.git",
"reference": "1049c0c15b6a1789fdeb52af688a94d540932469"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GoogleCloudPlatform/grpc-gcp-php/zipball/1049c0c15b6a1789fdeb52af688a94d540932469",
"reference": "1049c0c15b6a1789fdeb52af688a94d540932469",
"shasum": ""
},
"require": {
"google/auth": "^1.3",
"google/protobuf": "^v3.25.3||^4.26.1||^5.0",
"grpc/grpc": "^v1.13.0",
"php": "^8.0",
"psr/cache": "^1.0.1||^2.0.0||^3.0.0"
},
"require-dev": {
"google/cloud-spanner": "^1.7",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Grpc\\Gcp\\": "src/"
},
"classmap": [
"src/generated/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "gRPC GCP library for channel management",
"support": {
"issues": "https://github.com/GoogleCloudPlatform/grpc-gcp-php/issues",
"source": "https://github.com/GoogleCloudPlatform/grpc-gcp-php/tree/v0.4.2"
},
"time": "2026-03-12T22:56:09+00:00"
},
{
"name": "google/longrunning",
"version": "0.7.1",
"source": {
"type": "git",
"url": "https://github.com/googleapis/php-longrunning.git",
"reference": "cac9bedf199239ae2b1acd4a8e4ea2276bd9f55a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/php-longrunning/zipball/cac9bedf199239ae2b1acd4a8e4ea2276bd9f55a",
"reference": "cac9bedf199239ae2b1acd4a8e4ea2276bd9f55a",
"shasum": ""
},
"require-dev": {
"google/gax": "^1.38.0",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"extra": {
"component": {
"id": "longrunning",
"path": "LongRunning",
"entry": null,
"target": "googleapis/php-longrunning"
}
},
"autoload": {
"psr-4": {
"Google\\LongRunning\\": "src/LongRunning",
"Google\\ApiCore\\LongRunning\\": "src/ApiCore/LongRunning",
"GPBMetadata\\Google\\Longrunning\\": "metadata/Longrunning"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Google LongRunning Client for PHP",
"support": {
"source": "https://github.com/googleapis/php-longrunning/tree/v0.7.1"
},
"time": "2026-03-31T19:52:22+00:00"
},
{
"name": "google/protobuf",
"version": "v5.35.1",
"source": {
"type": "git",
"url": "https://github.com/protocolbuffers/protobuf-php.git",
"reference": "55bb4a7d6739b5af0927b96213c1371a3afb7cfb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/55bb4a7d6739b5af0927b96213c1371a3afb7cfb",
"reference": "55bb4a7d6739b5af0927b96213c1371a3afb7cfb",
"shasum": ""
},
"require": {
"php": ">=8.2.0"
},
"require-dev": {
"phpunit/phpunit": ">=11.5.0 <12.0.0"
},
"suggest": {
"ext-bcmath": "Need to support JSON deserialization"
},
"type": "library",
"autoload": {
"psr-4": {
"Google\\Protobuf\\": "src/Google/Protobuf",
"GPBMetadata\\Google\\Protobuf\\": "src/GPBMetadata/Google/Protobuf"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "proto library for PHP",
"homepage": "https://developers.google.com/protocol-buffers/",
"keywords": [
"proto"
],
"support": {
"source": "https://github.com/protocolbuffers/protobuf-php/tree/v5.35.1"
},
"time": "2026-06-11T21:19:23+00:00"
},
{
"name": "graham-campbell/result-type",
"version": "v1.1.4",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.5"
},
"require-dev": {
"phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7"
},
"type": "library",
"autoload": {
......@@ -640,6 +1478,50 @@
],
"time": "2025-12-27T19:43:20+00:00"
},
{
"name": "grpc/grpc",
"version": "1.82.0",
"source": {
"type": "git",
"url": "https://github.com/grpc/grpc-php.git",
"reference": "be984cb608f21e96453b3cfe54c748cc7b192250"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/grpc/grpc-php/zipball/be984cb608f21e96453b3cfe54c748cc7b192250",
"reference": "be984cb608f21e96453b3cfe54c748cc7b192250",
"shasum": ""
},
"require": {
"php": ">=7.1.0"
},
"require-dev": {
"google/auth": "^v1.3.0"
},
"suggest": {
"ext-protobuf": "For better performance, install the protobuf C extension.",
"google/protobuf": "To get started using grpc quickly, install the native protobuf library."
},
"type": "library",
"autoload": {
"psr-4": {
"Grpc\\": "src/lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "gRPC library for PHP",
"homepage": "https://grpc.io",
"keywords": [
"rpc"
],
"support": {
"source": "https://github.com/grpc/grpc-php/tree/v1.82.0"
},
"time": "2026-07-03T09:39:53+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "7.12.3",
......@@ -935,75 +1817,262 @@
"homepage": "https://github.com/Tobion"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.12.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
"type": "tidelift"
}
],
"time": "2026-06-23T15:21:08+00:00"
},
{
"name": "guzzlehttp/uri-template",
"version": "v1.0.8",
"source": {
"type": "git",
"url": "https://github.com/guzzle/uri-template.git",
"reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/uri-template/zipball/9c19128923b05a5d7355e5d2318d7808b7e33bbd",
"reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"symfony/polyfill-php80": "^1.25"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.52 || ^9.6.34",
"uri-template/tests": "1.0.0"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\UriTemplate\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
}
],
"description": "A polyfill class for uri_template of PHP",
"keywords": [
"guzzlehttp",
"uri-template"
],
"support": {
"issues": "https://github.com/guzzle/uri-template/issues",
"source": "https://github.com/guzzle/uri-template/tree/v1.0.8"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template",
"type": "tidelift"
}
],
"time": "2026-06-23T13:02:23+00:00"
},
{
"name": "kreait/firebase-php",
"version": "8.3.0",
"source": {
"type": "git",
"url": "https://github.com/beste/firebase-php.git",
"reference": "c73502749a91ee762bdfeff40c118058b6d5ff2c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/beste/firebase-php/zipball/c73502749a91ee762bdfeff40c118058b6d5ff2c",
"reference": "c73502749a91ee762bdfeff40c118058b6d5ff2c",
"shasum": ""
},
"require": {
"beste/clock": "^3.0",
"beste/in-memory-cache": "^1.3.1",
"beste/json": "^1.5.1",
"cuyz/valinor": "^2.2.1",
"ext-ctype": "*",
"ext-filter": "*",
"ext-json": "*",
"ext-mbstring": "*",
"fig/http-message-util": "^1.1.5",
"firebase/php-jwt": "^6.10.2 || ^7.0.2",
"google/auth": "^1.45",
"google/cloud-storage": "^1.50.0 || ^2.0.0",
"guzzlehttp/guzzle": "^7.9.2",
"guzzlehttp/promises": "^2.0.4",
"guzzlehttp/psr7": "^2.7",
"kreait/firebase-tokens": "^5.2",
"lcobucci/jwt": "^5.3",
"mtdowling/jmespath.php": "^2.9.2",
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
"psr/cache": "^2.0 || ^3.0",
"psr/clock": "^1.0",
"psr/http-client": "^1.0.3",
"psr/http-factory": "^1.1",
"psr/http-message": "^1.1 || ^2.0"
},
"require-dev": {
"google/cloud-firestore": "^1.55.0 || ^2.0",
"psr/log": "^3.0.2",
"symfony/var-dumper": "^7.4.4 || ^8.0.8",
"vlucas/phpdotenv": "^5.6.3"
},
"suggest": {
"google/cloud-firestore": "^1.55.0|^2.0 to use the Firestore component"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-7.x": "7.x-dev",
"dev-8.x": "8.x-dev"
}
},
"autoload": {
"psr-4": {
"Kreait\\Firebase\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jérôme Gamez",
"homepage": "https://github.com/jeromegamez"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"description": "Firebase Admin SDK",
"homepage": "https://github.com/beste/firebase-php",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
"api",
"database",
"firebase",
"google",
"sdk"
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.12.3"
"docs": "https://firebase-php.readthedocs.io",
"issues": "https://github.com/beste/firebase-php/issues",
"source": "https://github.com/beste/firebase-php"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"url": "https://github.com/sponsors/jeromegamez",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
"type": "tidelift"
}
],
"time": "2026-06-23T15:21:08+00:00"
"time": "2026-07-17T22:32:55+00:00"
},
{
"name": "guzzlehttp/uri-template",
"version": "v1.0.8",
"name": "kreait/firebase-tokens",
"version": "5.5.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/uri-template.git",
"reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd"
"url": "https://github.com/beste/firebase-tokens-php.git",
"reference": "a60e75e0a331754fbd02fd2544cbf5d91115c971"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/uri-template/zipball/9c19128923b05a5d7355e5d2318d7808b7e33bbd",
"reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd",
"url": "https://api.github.com/repos/beste/firebase-tokens-php/zipball/a60e75e0a331754fbd02fd2544cbf5d91115c971",
"reference": "a60e75e0a331754fbd02fd2544cbf5d91115c971",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"symfony/polyfill-php80": "^1.25"
"beste/clock": "^3.0",
"ext-json": "*",
"ext-openssl": "*",
"fig/http-message-util": "^1.1.5",
"guzzlehttp/guzzle": "^7.8.2 || ^8.0",
"lcobucci/jwt": "^5.2",
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
"psr/cache": "^1.0|^2.0|^3.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.52 || ^9.6.34",
"uri-template/tests": "1.0.0"
"beste/in-memory-cache": "^1.5",
"friendsofphp/php-cs-fixer": "^3.95.12",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.2.5",
"phpstan/phpstan-phpunit": "^2.0.18",
"phpunit/phpunit": "^10.5.64",
"rector/rector": "^2.5.4",
"symfony/var-dumper": "^6.4.3 || ^7.3.4 || ^8.1.1"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
"suggest": {
"psr/cache-implementation": "to cache fetched remote public keys"
},
"type": "library",
"autoload": {
"psr-4": {
"GuzzleHttp\\UriTemplate\\": "src"
"Kreait\\Firebase\\JWT\\": "src/JWT"
}
},
"notification-url": "https://packagist.org/downloads/",
......@@ -1012,50 +2081,30 @@
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
"name": "Jérôme Gamez",
"homepage": "https://github.com/jeromegamez"
}
],
"description": "A polyfill class for uri_template of PHP",
"description": "A library to work with Firebase tokens",
"homepage": "https://github.com/beste/firebase-token-php",
"keywords": [
"guzzlehttp",
"uri-template"
"Authentication",
"auth",
"firebase",
"google",
"token"
],
"support": {
"issues": "https://github.com/guzzle/uri-template/issues",
"source": "https://github.com/guzzle/uri-template/tree/v1.0.8"
"issues": "https://github.com/beste/firebase-tokens-php/issues",
"source": "https://github.com/beste/firebase-tokens-php/tree/5.5.0"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"url": "https://github.com/sponsors/jeromegamez",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template",
"type": "tidelift"
}
],
"time": "2026-06-23T13:02:23+00:00"
"time": "2026-07-23T07:33:29+00:00"
},
{
"name": "laravel/framework",
......@@ -1533,6 +2582,79 @@
},
"time": "2026-03-17T14:54:13+00:00"
},
{
"name": "lcobucci/jwt",
"version": "5.6.0",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/jwt.git",
"reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e",
"reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"ext-sodium": "*",
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"psr/clock": "^1.0"
},
"require-dev": {
"infection/infection": "^0.29",
"lcobucci/clock": "^3.2",
"lcobucci/coding-standard": "^11.0",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.2",
"phpstan/phpstan": "^1.10.7",
"phpstan/phpstan-deprecation-rules": "^1.1.3",
"phpstan/phpstan-phpunit": "^1.3.10",
"phpstan/phpstan-strict-rules": "^1.5.0",
"phpunit/phpunit": "^11.1"
},
"suggest": {
"lcobucci/clock": ">= 3.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Lcobucci\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Luís Cobucci",
"email": "lcobucci@gmail.com",
"role": "Developer"
}
],
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
"keywords": [
"JWS",
"jwt"
],
"support": {
"issues": "https://github.com/lcobucci/jwt/issues",
"source": "https://github.com/lcobucci/jwt/tree/5.6.0"
},
"funding": [
{
"url": "https://github.com/lcobucci",
"type": "github"
},
{
"url": "https://www.patreon.com/lcobucci",
"type": "patreon"
}
],
"time": "2025-10-17T11:30:53+00:00"
},
{
"name": "league/commonmark",
"version": "2.8.2",
......@@ -2271,6 +3393,72 @@
],
"time": "2026-01-02T08:56:05+00:00"
},
{
"name": "mtdowling/jmespath.php",
"version": "2.9.2",
"source": {
"type": "git",
"url": "https://github.com/jmespath/jmespath.php.git",
"reference": "2157c5e50e813ec6a96c1eed3be7f64a20fb32a8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/2157c5e50e813ec6a96c1eed3be7f64a20fb32a8",
"reference": "2157c5e50e813ec6a96c1eed3be7f64a20fb32a8",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"symfony/polyfill-mbstring": "^1.17"
},
"require-dev": {
"composer/xdebug-handler": "^3.0.3",
"phpunit/phpunit": "^8.5.52"
},
"bin": [
"bin/jp.php"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.9-dev"
}
},
"autoload": {
"files": [
"src/JmesPath.php"
],
"psr-4": {
"JmesPath\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Declaratively specify how to extract elements from a JSON document",
"keywords": [
"json",
"jsonpath"
],
"support": {
"issues": "https://github.com/jmespath/jmespath.php/issues",
"source": "https://github.com/jmespath/jmespath.php/tree/2.9.2"
},
"time": "2026-07-06T18:56:19+00:00"
},
{
"name": "nesbot/carbon",
"version": "3.13.0",
......@@ -2754,6 +3942,55 @@
],
"time": "2025-12-27T19:41:33+00:00"
},
{
"name": "psr/cache",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/cache.git",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for caching libraries",
"keywords": [
"cache",
"psr",
"psr-6"
],
"support": {
"source": "https://github.com/php-fig/cache/tree/3.0.0"
},
"time": "2021-02-03T23:26:27+00:00"
},
{
"name": "psr/clock",
"version": "1.0.0",
......@@ -3443,6 +4680,70 @@
},
"time": "2026-06-18T03:57:49+00:00"
},
{
"name": "rize/uri-template",
"version": "0.4.2",
"source": {
"type": "git",
"url": "https://github.com/rize/UriTemplate.git",
"reference": "7ad22944daede547b4542e1c977ec4a81aa20832"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rize/UriTemplate/zipball/7ad22944daede547b4542e1c977ec4a81aa20832",
"reference": "7ad22944daede547b4542e1c977ec4a81aa20832",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.63",
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "~10.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Rize\\": "src/Rize"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marut K",
"homepage": "http://twitter.com/rezigned"
}
],
"description": "PHP URI Template (RFC 6570) supports both expansion & extraction",
"keywords": [
"RFC 6570",
"template",
"uri"
],
"support": {
"issues": "https://github.com/rize/UriTemplate/issues",
"source": "https://github.com/rize/UriTemplate/tree/0.4.2"
},
"funding": [
{
"url": "https://www.paypal.me/rezigned",
"type": "custom"
},
{
"url": "https://github.com/rezigned",
"type": "github"
},
{
"url": "https://opencollective.com/rize-uri-template",
"type": "open_collective"
}
],
"time": "2026-05-07T15:30:40+00:00"
},
{
"name": "symfony/clock",
"version": "v8.1.0",
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasColumn('notification_preferences', 'channel_push')) {
Schema::table('notification_preferences', function (Blueprint $table) {
$table->boolean('channel_push')->default(true)->after('channel_sms');
});
}
}
public function down(): void
{
Schema::table('notification_preferences', function (Blueprint $table) {
$table->dropColumn('channel_push');
});
}
};
......@@ -3,6 +3,7 @@
use App\Http\Controllers\Api\V1\AppConfigController;
use App\Http\Controllers\Api\V1\AuthOtpController;
use App\Http\Controllers\Api\V1\DeviceController;
use App\Http\Controllers\Api\V1\NotificationController;
use App\Http\Controllers\Api\V1\ParticipantController;
use Illuminate\Support\Facades\Route;
......@@ -39,5 +40,12 @@
Route::get('invoices', [ParticipantController::class, 'invoices']);
Route::get('enrollments', [ParticipantController::class, 'enrollments']);
});
// Notifications
Route::get('notifications', [NotificationController::class, 'index']);
Route::patch('notifications/{id}/read', [NotificationController::class, 'markAsRead']);
Route::post('notifications/read-all', [NotificationController::class, 'markAllAsRead']);
Route::get('notifications/preferences', [NotificationController::class, 'getPreferences']);
Route::post('notifications/preferences', [NotificationController::class, 'updatePreferences']);
});
});
......@@ -25,3 +25,8 @@
Schedule::command('reports:parent-weekly')->weeklyOn(6, '12:00');
Schedule::command('enrollments:generate-renewals')->dailyAt('07:00');
Schedule::command('groups:alert-capacity --threshold=90')->dailyAt('08:00');
// Push notifications
Schedule::command('push:session-reminder --minutes=30')->everyMinute();
Schedule::command('push:installment-due --days=1')->dailyAt('09:00');
Schedule::command('push:installment-due --days=3')->dailyAt('09:30');
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment