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,51 @@
<?php
namespace Tests\Unit\Resources\Notifications;
use App\Http\Resources\Notifications\NotificationResource;
use App\Models\Notification;
use Tests\TestCase;
class NotificationResourceTest extends TestCase
{
public function test_resource_returns_expected_shape(): void
{
$notification = new Notification([
'id' => 1,
'title' => 'Title',
'message' => 'Message',
'target_group' => 'parent',
'delivery_channels' => ['in_app'],
'priority' => 'normal',
'status' => 'sent',
'action_url' => 'https://example.test',
'attachment_path' => '/path/file.pdf',
'scheduled_at' => now(),
'sent_at' => now(),
'expires_at' => now()->addDay(),
'school_year' => '2025-2026',
'semester' => 'Fall',
'created_at' => now(),
'updated_at' => now(),
]);
$payload = (new NotificationResource($notification))->toArray(request());
$this->assertArrayHasKey('id', $payload);
$this->assertArrayHasKey('title', $payload);
$this->assertArrayHasKey('message', $payload);
$this->assertArrayHasKey('target_group', $payload);
$this->assertArrayHasKey('delivery_channels', $payload);
$this->assertArrayHasKey('priority', $payload);
$this->assertArrayHasKey('status', $payload);
$this->assertArrayHasKey('action_url', $payload);
$this->assertArrayHasKey('attachment_path', $payload);
$this->assertArrayHasKey('scheduled_at', $payload);
$this->assertArrayHasKey('sent_at', $payload);
$this->assertArrayHasKey('expires_at', $payload);
$this->assertArrayHasKey('school_year', $payload);
$this->assertArrayHasKey('semester', $payload);
$this->assertArrayHasKey('created_at', $payload);
$this->assertArrayHasKey('updated_at', $payload);
}
}
@@ -0,0 +1,35 @@
<?php
namespace Tests\Unit\Resources\Notifications;
use App\Http\Resources\Notifications\UserNotificationResource;
use Tests\TestCase;
class UserNotificationResourceTest extends TestCase
{
public function test_resource_returns_expected_shape(): void
{
$resource = new UserNotificationResource([
'notification_id' => 10,
'title' => 'Title',
'message' => 'Message',
'priority' => 'high',
'scheduled_at' => '2025-01-01 00:00:00',
'expires_at' => '2025-01-02 00:00:00',
'target_group' => 'parent',
'is_read' => 1,
]);
$payload = $resource->toArray(request());
$this->assertArrayHasKey('notification_id', $payload);
$this->assertArrayHasKey('title', $payload);
$this->assertArrayHasKey('message', $payload);
$this->assertArrayHasKey('priority', $payload);
$this->assertArrayHasKey('scheduled_at', $payload);
$this->assertArrayHasKey('expires_at', $payload);
$this->assertArrayHasKey('target_group', $payload);
$this->assertArrayHasKey('is_read', $payload);
$this->assertTrue($payload['is_read']);
}
}
@@ -24,7 +24,7 @@ class AdministratorAbsenceServiceTest extends TestCase
new \App\Models\User(),
new \App\Models\StaffAttendance(),
new \App\Services\SemesterRangeService(new \App\Services\Semesters\SemesterConfigService()),
Mockery::mock(\App\Libraries\StaffTimeOffLinkService::class)
Mockery::mock(\App\Services\Staff\StaffTimeOffLinkService::class)
);
$request = Request::create('/absences', 'POST', ['dates' => ['2025-01-05']]);
@@ -0,0 +1,68 @@
<?php
namespace Tests\Unit\Services\Attendance;
use App\Services\Attendance\AttendanceAutoPublishJobService;
use App\Services\Attendance\AttendanceAutoPublishService;
use DateTimeImmutable;
use DateTimeZone;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AttendanceAutoPublishJobServiceTest extends TestCase
{
use RefreshDatabase;
public function test_run_publishes_due_records(): void
{
$autoPublish = new AttendanceAutoPublishService();
$service = new AttendanceAutoPublishJobService($autoPublish);
$now = new DateTimeImmutable('2025-03-01 10:00:00', new DateTimeZone('UTC'));
$cutoff = $autoPublish->secondSundayBackwardDate($now);
DB::table('attendance_day')->insert([
[
'class_section_id' => 1,
'date' => '2025-03-01',
'status' => 'submitted',
'auto_publish_at' => '2025-03-01 09:00:00',
'semester' => 'Spring',
'school_year' => '2024-2025',
],
[
'class_section_id' => 2,
'date' => $cutoff,
'status' => 'submitted',
'auto_publish_at' => null,
'semester' => 'Spring',
'school_year' => '2024-2025',
],
[
'class_section_id' => 3,
'date' => '2025-02-20',
'status' => 'draft',
'auto_publish_at' => null,
'semester' => 'Spring',
'school_year' => '2024-2025',
],
]);
$result = $service->run($now);
$this->assertSame(2, $result['published']);
$this->assertDatabaseHas('attendance_day', [
'class_section_id' => 1,
'status' => 'published',
]);
$this->assertDatabaseHas('attendance_day', [
'class_section_id' => 2,
'status' => 'published',
]);
$this->assertDatabaseHas('attendance_day', [
'class_section_id' => 3,
'status' => 'draft',
]);
}
}
@@ -0,0 +1,29 @@
<?php
namespace Tests\Unit\Services\Attendance;
use App\Services\Attendance\AttendanceAutoPublishService;
use DateTimeImmutable;
use Tests\TestCase;
class AttendanceAutoPublishServiceTest extends TestCase
{
public function test_second_sunday_after_end_of_day(): void
{
$service = new AttendanceAutoPublishService();
$result = $service->secondSundayAfterEndOfDay('2025-02-03', 'UTC');
$this->assertSame('2025-02-16 23:59:59', $result);
}
public function test_second_sunday_backward_date(): void
{
$service = new AttendanceAutoPublishService();
$now = new DateTimeImmutable('2025-02-16 10:00:00', new \DateTimeZone('UTC'));
$result = $service->secondSundayBackwardDate($now, 'UTC');
$this->assertSame('2025-02-02', $result);
}
}
@@ -0,0 +1,79 @@
<?php
namespace Tests\Unit\Services\Attendance;
use App\Models\AttendanceTracking;
use App\Services\Attendance\AttendanceConsequenceService;
use App\Services\EmailService;
use App\Services\Notifications\UserNotificationDispatchService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class AttendanceConsequenceServiceTest extends TestCase
{
use RefreshDatabase;
public function test_send_follow_up_requires_parent_email(): void
{
$service = new AttendanceConsequenceService(
Mockery::mock(EmailService::class),
Mockery::mock(UserNotificationDispatchService::class)
);
$result = $service->sendFollowUp([
'parent' => ['user_id' => 5],
]);
$this->assertFalse($result['ok']);
}
public function test_send_follow_up_marks_tracking_and_notifies(): void
{
\Illuminate\Support\Facades\DB::table('students')->insert([
'id' => 1,
'school_id' => 1,
'parent_id' => 1,
'firstname' => 'Student',
'lastname' => 'One',
'dob' => '2015-01-01',
'age' => 10,
'gender' => 'Male',
'is_active' => 1,
'photo_consent' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
AttendanceTracking::query()->create([
'student_id' => 1,
'date' => now(),
'is_reported' => 1,
'reason' => 'ABS_2',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$email = Mockery::mock(EmailService::class);
$email->shouldReceive('send')->once()->andReturn(true);
$notifier = Mockery::mock(UserNotificationDispatchService::class);
$notifier->shouldReceive('notifyUser')->once();
$service = new AttendanceConsequenceService($email, $notifier);
$result = $service->sendFollowUp([
'student_id' => 1,
'parent' => ['email' => 'parent@example.com', 'user_id' => 5],
'student' => ['firstname' => 'A', 'lastname' => 'B'],
'tracking_id' => 1,
'html' => '<p>Test</p>',
]);
$this->assertTrue($result['ok']);
$this->assertDatabaseHas('attendance_tracking', [
'id' => 1,
'is_notified' => 1,
]);
}
}
@@ -0,0 +1,68 @@
<?php
namespace Tests\Unit\Services\Attendance;
use App\Services\Attendance\AttendanceDailySummaryService;
use App\Services\EmailService;
use App\Services\Notifications\UserNotificationDispatchService;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class AttendanceDailySummaryServiceTest extends TestCase
{
use RefreshDatabase;
public function test_send_absentees_summary_notifies_parent(): void
{
Carbon::setTestNow(Carbon::parse('2025-02-10 08:00:00'));
DB::table('users')->insert([
'id' => 5,
'firstname' => 'Parent',
'lastname' => 'One',
'email' => 'parent@example.com',
'status' => 'Active',
]);
DB::table('students')->insert([
'id' => 10,
'school_id' => 1,
'parent_id' => 5,
'firstname' => 'Student',
'lastname' => 'One',
'dob' => '2015-01-01',
'age' => 10,
'gender' => 'Male',
'is_active' => 1,
'photo_consent' => 1,
'year_of_registration' => '2025',
'school_year' => '2024-2025',
]);
DB::table('attendance_data')->insert([
'class_id' => 1,
'class_section_id' => 1,
'student_id' => 10,
'school_id' => 'S1',
'date' => '2025-02-10',
'status' => 'absent',
'semester' => 'Spring',
'school_year' => '2024-2025',
]);
$notifier = Mockery::mock(UserNotificationDispatchService::class);
$notifier->shouldReceive('notifyUser')->once();
$email = Mockery::mock(EmailService::class);
$email->shouldReceive('send')->once()->andReturn(true);
$service = new AttendanceDailySummaryService($notifier, $email);
$count = $service->sendAbsenteesSummary();
$this->assertSame(1, $count);
}
}
@@ -39,4 +39,12 @@ class AttendancePolicyServiceTest extends TestCase
$this->assertTrue($isAdmin);
}
public function test_can_manage_early_dismissals_for_teacher(): void
{
$service = new AttendancePolicyService();
$allowed = $service->canManageEarlyDismissals(['Teacher']);
$this->assertTrue($allowed);
}
}
@@ -95,7 +95,8 @@ class AttendanceQueryServiceTest extends TestCase
new \App\Models\AttendanceData(),
new \App\Models\Student(),
new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord())
)
),
new \App\Services\Attendance\AttendanceAutoPublishService()
),
new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord())
),
@@ -0,0 +1,68 @@
<?php
namespace Tests\Unit\Services\Attendance;
use App\Services\Attendance\AttendanceSummaryRebuildService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AttendanceSummaryRebuildServiceTest extends TestCase
{
use RefreshDatabase;
public function test_rebuild_inserts_summary_rows(): void
{
DB::table('attendance_data')->insert([
[
'class_id' => 1,
'class_section_id' => 10,
'student_id' => 1,
'school_id' => 'S1',
'date' => '2025-02-01',
'status' => 'present',
'semester' => 'Spring',
'school_year' => '2024-2025',
],
[
'class_id' => 1,
'class_section_id' => 10,
'student_id' => 1,
'school_id' => 'S1',
'date' => '2025-02-02',
'status' => 'absent',
'semester' => 'Spring',
'school_year' => '2024-2025',
],
[
'class_id' => 1,
'class_section_id' => 11,
'student_id' => 2,
'school_id' => 'S1',
'date' => '2025-02-02',
'status' => 'late',
'semester' => 'Spring',
'school_year' => '2024-2025',
],
]);
$service = new AttendanceSummaryRebuildService();
$result = $service->rebuild();
$this->assertSame(2, $result['inserted']);
$this->assertDatabaseHas('attendance_record', [
'student_id' => 1,
'total_presence' => 1,
'total_absence' => 1,
'total_late' => 0,
'total_attendance' => 2,
]);
$this->assertDatabaseHas('attendance_record', [
'student_id' => 2,
'total_presence' => 0,
'total_absence' => 0,
'total_late' => 1,
'total_attendance' => 1,
]);
}
}
@@ -39,7 +39,8 @@ class TeacherAttendanceSubmissionServiceTest extends TestCase
new \App\Models\AttendanceData(),
new \App\Models\Student(),
new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord())
)
),
new \App\Services\Attendance\AttendanceAutoPublishService()
);
$this->expectException(\RuntimeException::class);
@@ -0,0 +1,27 @@
<?php
namespace Tests\Unit\Services\Auth;
use App\Services\Auth\PasswordResetCleanupService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class PasswordResetCleanupServiceTest extends TestCase
{
use RefreshDatabase;
public function test_cleanup_removes_old_requests(): void
{
DB::table('password_reset_requests')->insert([
['ip_address' => '127.0.0.1', 'requested_at' => now()->subDays(40)],
['ip_address' => '127.0.0.1', 'requested_at' => now()->subDays(5)],
]);
$service = new PasswordResetCleanupService();
$deleted = $service->cleanup(30);
$this->assertSame(1, $deleted);
$this->assertDatabaseCount('password_reset_requests', 1);
}
}
@@ -0,0 +1,102 @@
<?php
namespace Tests\Unit\Services\Grading;
use App\Services\EmailService;
use App\Services\Grading\BelowSixtyEmailService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class BelowSixtyEmailServiceTest extends TestCase
{
use RefreshDatabase;
public function test_send_requires_student_id(): void
{
$service = new BelowSixtyEmailService(Mockery::mock(EmailService::class));
$result = $service->send([]);
$this->assertFalse($result['ok']);
}
public function test_send_delivers_to_guardians(): void
{
DB::table('families')->insert([
'id' => 1,
'family_code' => 'FAM-1',
'household_name' => 'Family',
'is_active' => 1,
]);
DB::table('users')->insert([
'id' => 10,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '5551112222',
'email' => 'parent@example.com',
'address_street' => '123 Main',
'city' => 'City',
'state' => 'ST',
'zip' => '12345',
'accept_school_policy' => 1,
'is_verified' => 1,
'status' => 'Active',
'is_suspended' => 0,
'failed_attempts' => 0,
'password' => bcrypt('secret'),
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('family_guardians')->insert([
'family_id' => 1,
'user_id' => 10,
'relation' => 'primary',
'is_primary' => 1,
'receive_emails' => 1,
'receive_sms' => 0,
]);
DB::table('students')->insert([
'id' => 22,
'school_id' => 1,
'parent_id' => 10,
'firstname' => 'Student',
'lastname' => 'One',
'dob' => '2015-01-01',
'age' => 10,
'gender' => 'Male',
'is_active' => 1,
'photo_consent' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
DB::table('family_students')->insert([
'family_id' => 1,
'student_id' => 22,
'is_primary_home' => 1,
]);
$email = Mockery::mock(EmailService::class);
$email->shouldReceive('send')->once()->andReturn(true);
$service = new BelowSixtyEmailService($email);
$result = $service->send([
'student_id' => 22,
'student_name' => 'Student',
'class_section_name' => 'Class',
'semester' => 'Fall',
'school_year' => '2025-2026',
'scores' => ['semester_score' => 55],
'comment' => 'Needs improvement',
'html' => '<p>Report</p>',
]);
$this->assertTrue($result['ok']);
$this->assertSame(1, $result['sent']);
}
}
@@ -0,0 +1,47 @@
<?php
namespace Tests\Unit\Services\Notifications;
use App\Services\Notifications\NotificationActiveService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class NotificationActiveServiceTest extends TestCase
{
use RefreshDatabase;
public function test_list_filters_by_target_group(): void
{
DB::table('notifications')->insert([
[
'id' => 1,
'title' => 'Parent',
'message' => 'Message',
'target_group' => 'parent',
'delivery_channels' => json_encode(['in_app']),
'scheduled_at' => now()->subHour(),
'expires_at' => now()->addHour(),
'created_at' => now(),
'updated_at' => now(),
],
[
'id' => 2,
'title' => 'Teacher',
'message' => 'Message',
'target_group' => 'teacher',
'delivery_channels' => json_encode(['in_app']),
'scheduled_at' => now()->subHour(),
'expires_at' => now()->addHour(),
'created_at' => now(),
'updated_at' => now(),
],
]);
$service = new NotificationActiveService();
$result = $service->list('parent');
$this->assertCount(1, $result['notifications']);
$this->assertFalse($result['notifications'][0]['isExpired']);
}
}
@@ -0,0 +1,52 @@
<?php
namespace Tests\Unit\Services\Notifications;
use App\Services\Notifications\NotificationCleanupService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class NotificationCleanupServiceTest extends TestCase
{
use RefreshDatabase;
public function test_cleanup_expired_soft_deletes_rows(): void
{
DB::table('notifications')->insert([
[
'id' => 1,
'title' => 'Old',
'message' => 'Old',
'target_group' => 'all',
'delivery_channels' => json_encode(['in_app']),
'priority' => 1,
'status' => 'sent',
'scheduled_at' => now()->subDays(2),
'expires_at' => now()->subDay(),
'school_year' => '2024-2025',
'semester' => 'Spring',
],
[
'id' => 2,
'title' => 'Active',
'message' => 'Active',
'target_group' => 'all',
'delivery_channels' => json_encode(['in_app']),
'priority' => 1,
'status' => 'sent',
'scheduled_at' => now()->subDay(),
'expires_at' => now()->addDay(),
'school_year' => '2024-2025',
'semester' => 'Spring',
],
]);
$service = new NotificationCleanupService();
$deleted = $service->cleanupExpired();
$this->assertSame(1, $deleted);
$this->assertSoftDeleted('notifications', ['id' => 1]);
$this->assertDatabaseHas('notifications', ['id' => 2]);
}
}
@@ -0,0 +1,33 @@
<?php
namespace Tests\Unit\Services\Notifications;
use App\Services\Notifications\NotificationDeletedService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class NotificationDeletedServiceTest extends TestCase
{
use RefreshDatabase;
public function test_list_returns_deleted_notifications(): void
{
DB::table('notifications')->insert([
'id' => 1,
'title' => 'Deleted',
'message' => 'Message',
'target_group' => 'parent',
'delivery_channels' => json_encode(['in_app']),
'deleted_at' => now(),
'created_at' => now(),
'updated_at' => now(),
]);
$service = new NotificationDeletedService();
$result = $service->list();
$this->assertCount(1, $result['notifications']);
$this->assertSame(1, $result['notifications'][0]['id']);
}
}
@@ -0,0 +1,21 @@
<?php
namespace Tests\Unit\Services\Notifications;
use App\Services\Notifications\NotificationDispatchService;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;
class NotificationDispatchServiceTest extends TestCase
{
public function test_dispatch_logs_email_and_sms(): void
{
Log::spy();
$service = new NotificationDispatchService();
$service->sendEmail('parent@example.com', 'Subject', 'Message');
$service->sendSms('5551112222', 'Message');
Log::shouldHaveReceived('info')->twice();
}
}
@@ -0,0 +1,66 @@
<?php
namespace Tests\Unit\Services\Notifications;
use App\Services\Notifications\NotificationManagementService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class NotificationManagementServiceTest extends TestCase
{
use RefreshDatabase;
public function test_update_modifies_notification(): void
{
DB::table('notifications')->insert([
'id' => 1,
'title' => 'Old',
'message' => 'Message',
'target_group' => 'parent',
'delivery_channels' => json_encode(['in_app']),
'created_at' => now(),
'updated_at' => now(),
]);
$service = new NotificationManagementService();
$result = $service->update(1, ['title' => 'New']);
$this->assertTrue($result['ok']);
$this->assertDatabaseHas('notifications', [
'id' => 1,
'title' => 'New',
]);
}
public function test_update_returns_false_when_missing(): void
{
$service = new NotificationManagementService();
$result = $service->update(999, ['title' => 'New']);
$this->assertFalse($result['ok']);
}
public function test_delete_and_restore_notification(): void
{
DB::table('notifications')->insert([
'id' => 1,
'title' => 'Old',
'message' => 'Message',
'target_group' => 'parent',
'delivery_channels' => json_encode(['in_app']),
'created_at' => now(),
'updated_at' => now(),
]);
$service = new NotificationManagementService();
$this->assertTrue($service->delete(1));
$this->assertSoftDeleted('notifications', ['id' => 1]);
$this->assertTrue($service->restore(1));
$this->assertDatabaseHas('notifications', [
'id' => 1,
'deleted_at' => null,
]);
}
}
@@ -0,0 +1,39 @@
<?php
namespace Tests\Unit\Services\Notifications;
use App\Services\Notifications\NotificationReadService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class NotificationReadServiceTest extends TestCase
{
use RefreshDatabase;
public function test_mark_read_updates_row(): void
{
DB::table('user_notifications')->insert([
'notification_id' => 1,
'user_id' => 1,
'is_read' => 0,
]);
$service = new NotificationReadService();
$result = $service->markRead(1, 1);
$this->assertTrue($result);
$this->assertDatabaseHas('user_notifications', [
'notification_id' => 1,
'user_id' => 1,
'is_read' => 1,
]);
}
public function test_mark_read_returns_false_when_missing(): void
{
$service = new NotificationReadService();
$this->assertFalse($service->markRead(1, 99));
}
}
@@ -0,0 +1,55 @@
<?php
namespace Tests\Unit\Services\Notifications;
use App\Services\Notifications\NotificationRecipientService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class NotificationRecipientServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_recipients_by_role(): void
{
DB::table('roles')->insert([
'id' => 1,
'name' => 'parent',
'slug' => 'parent',
'is_active' => 1,
]);
DB::table('users')->insert([
'id' => 10,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '5551112222',
'email' => 'parent@example.com',
'address_street' => '123 Main',
'city' => 'City',
'state' => 'ST',
'zip' => '12345',
'accept_school_policy' => 1,
'is_verified' => 1,
'status' => 'Active',
'is_suspended' => 0,
'failed_attempts' => 0,
'password' => bcrypt('secret'),
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('user_roles')->insert([
'user_id' => 10,
'role_id' => 1,
]);
$service = new NotificationRecipientService();
$rows = $service->getRecipients('parent');
$this->assertCount(1, $rows);
$this->assertSame('parent@example.com', $rows[0]['email']);
}
}
@@ -0,0 +1,50 @@
<?php
namespace Tests\Unit\Services\Notifications;
use App\Models\Notification;
use App\Models\UserNotification;
use App\Services\Notifications\NotificationDispatchService;
use App\Services\Notifications\NotificationRecipientService;
use App\Services\Notifications\NotificationSendService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class NotificationSendServiceTest extends TestCase
{
use RefreshDatabase;
public function test_send_creates_notification_and_user_notifications(): void
{
$recipients = Mockery::mock(NotificationRecipientService::class);
$recipients->shouldReceive('getRecipients')
->with('parent')
->andReturn([
['id' => 10, 'email' => 'parent1@example.com', 'phone' => '5551112222'],
['id' => 11, 'email' => 'parent2@example.com', 'phone' => null],
]);
$dispatcher = Mockery::mock(NotificationDispatchService::class);
$dispatcher->shouldReceive('sendEmail')->once();
$dispatcher->shouldReceive('sendSms')->once();
$service = new NotificationSendService($recipients, $dispatcher);
$result = $service->send([
'title' => 'Announcement',
'message' => 'Hello parents',
'target_group' => 'parent',
'channels' => ['in_app', 'email', 'sms'],
], 99);
$this->assertTrue($result['ok']);
$this->assertSame(2, $result['recipient_count']);
$this->assertDatabaseHas('notifications', [
'title' => 'Announcement',
'target_group' => 'parent',
]);
$this->assertSame(1, Notification::query()->count());
$this->assertSame(2, UserNotification::query()->count());
}
}
@@ -0,0 +1,47 @@
<?php
namespace Tests\Unit\Services\Notifications;
use App\Services\Notifications\NotificationShowService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class NotificationShowServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_for_user_returns_notification(): void
{
DB::table('notifications')->insert([
'id' => 1,
'title' => 'Hello',
'message' => 'Message',
'target_group' => 'parent',
'delivery_channels' => json_encode(['in_app']),
'scheduled_at' => now()->subHour(),
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('user_notifications')->insert([
'notification_id' => 1,
'user_id' => 1,
'is_read' => 0,
]);
$service = new NotificationShowService();
$row = $service->getForUser(1, 1);
$this->assertNotNull($row);
$this->assertSame(1, $row['notification_id']);
}
public function test_get_for_user_returns_null_when_missing(): void
{
$service = new NotificationShowService();
$row = $service->getForUser(1, 99);
$this->assertNull($row);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Tests\Unit\Services\Notifications;
use App\Services\Notifications\NotificationTriggerService;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class NotificationTriggerServiceTest extends TestCase
{
public function test_trigger_dispatches_event(): void
{
Event::fake();
NotificationTriggerService::trigger('customNotification', ['foo' => 'bar']);
Event::assertDispatched('customNotification');
}
public function test_to_user_dispatches_event(): void
{
Event::fake();
NotificationTriggerService::toUser(5, 'Hello', 'Message');
Event::assertDispatched('customNotification');
}
}
@@ -0,0 +1,60 @@
<?php
namespace Tests\Unit\Services\Notifications;
use App\Services\Notifications\NotificationUserListService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class NotificationUserListServiceTest extends TestCase
{
use RefreshDatabase;
public function test_list_filters_by_read_status(): void
{
DB::table('notifications')->insert([
[
'id' => 1,
'title' => 'Unread',
'message' => 'Message 1',
'target_group' => 'parent',
'delivery_channels' => json_encode(['in_app']),
'scheduled_at' => now()->subHour(),
'created_at' => now(),
'updated_at' => now(),
],
[
'id' => 2,
'title' => 'Read',
'message' => 'Message 2',
'target_group' => 'parent',
'delivery_channels' => json_encode(['in_app']),
'scheduled_at' => now()->subHours(2),
'created_at' => now(),
'updated_at' => now(),
],
]);
DB::table('user_notifications')->insert([
[
'notification_id' => 1,
'user_id' => 1,
'is_read' => 0,
],
[
'notification_id' => 2,
'user_id' => 1,
'is_read' => 1,
],
]);
$service = new NotificationUserListService();
$result = $service->listForUser(1, [
'read' => true,
'per_page' => 10,
]);
$this->assertSame(1, $result['pagination']['total']);
}
}
@@ -0,0 +1,58 @@
<?php
namespace Tests\Unit\Services\Notifications;
use App\Services\Notifications\UserNotificationDispatchService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class UserNotificationDispatchServiceTest extends TestCase
{
use RefreshDatabase;
public function test_notify_user_creates_notification(): void
{
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '5551112222',
'email' => 'parent@example.com',
'address_street' => '123 Main',
'city' => 'City',
'state' => 'ST',
'zip' => '12345',
'accept_school_policy' => 1,
'is_verified' => 1,
'status' => 'Active',
'is_suspended' => 0,
'failed_attempts' => 0,
'password' => bcrypt('secret'),
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$service = new UserNotificationDispatchService();
$result = $service->notifyUser(1, 'Title', 'Message', ['in_app'], 'registration', 'parent');
$this->assertTrue($result['ok']);
$this->assertDatabaseHas('notifications', [
'title' => 'Title',
'target_group' => 'parent',
]);
$this->assertDatabaseHas('user_notifications', [
'user_id' => 1,
'is_read' => 0,
]);
}
public function test_notify_user_rejects_invalid_user(): void
{
$service = new UserNotificationDispatchService();
$result = $service->notifyUser(0, 'Title', 'Message');
$this->assertFalse($result['ok']);
}
}
@@ -0,0 +1,77 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\EmailService;
use App\Services\Notifications\UserNotificationDispatchService;
use App\Services\Payments\PaymentMissedCheckService;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class PaymentMissedCheckServiceTest extends TestCase
{
use RefreshDatabase;
public function test_find_users_with_missed_payments(): void
{
Carbon::setTestNow(Carbon::parse('2025-03-01'));
DB::table('users')->insert([
'id' => 1,
'firstname' => 'Parent',
'lastname' => 'Late',
'email' => 'late@example.com',
'status' => 'Active',
]);
DB::table('invoices')->insert([
'id' => 1,
'parent_id' => 1,
'invoice_number' => 'INV-1',
'total_amount' => 100,
'balance' => 50,
'paid_amount' => 50,
'status' => 'Unpaid',
'school_year' => '2024-2025',
'semester' => 'Spring',
'issue_date' => '2025-01-01',
'due_date' => '2025-02-01',
]);
$service = new PaymentMissedCheckService(
Mockery::mock(UserNotificationDispatchService::class),
Mockery::mock(EmailService::class)
);
$users = $service->findUsersWithMissedPayments();
$this->assertCount(1, $users);
$this->assertSame(1, (int) $users[0]['user_id']);
}
public function test_send_reminders_sends_email_and_notification(): void
{
$notifier = Mockery::mock(UserNotificationDispatchService::class);
$notifier->shouldReceive('notifyUser')->once();
$email = Mockery::mock(EmailService::class);
$email->shouldReceive('send')->once()->andReturn(true);
$service = new PaymentMissedCheckService($notifier, $email);
$result = $service->sendReminders([
[
'user_id' => 5,
'firstname' => 'Parent',
'lastname' => 'User',
'email' => 'parent@example.com',
],
]);
$this->assertSame(1, $result['sent']);
$this->assertSame(0, $result['failed']);
}
}
@@ -0,0 +1,79 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\EmailService;
use App\Services\Payments\PaymentTestNotificationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class PaymentTestNotificationServiceTest extends TestCase
{
use RefreshDatabase;
public function test_send_logs_and_sends_email(): void
{
DB::table('configuration')->insert([
['config_key' => 'school_year', 'config_value' => '2024-2025'],
['config_key' => 'installment_date', 'config_value' => '2025-06-01'],
]);
DB::table('users')->insert([
['id' => 10, 'firstname' => 'Primary', 'lastname' => 'Parent', 'email' => 'primary@example.com', 'status' => 'Active'],
['id' => 11, 'firstname' => 'Secondary', 'lastname' => 'Parent', 'email' => 'secondary@example.com', 'status' => 'Active'],
]);
DB::table('families')->insert([
'id' => 1,
'family_code' => 'FAM-1',
'is_active' => 1,
]);
DB::table('family_guardians')->insert([
[
'family_id' => 1,
'user_id' => 10,
'relation' => 'guardian',
'is_primary' => 1,
'receive_emails' => 1,
'receive_sms' => 0,
],
[
'family_id' => 1,
'user_id' => 11,
'relation' => 'guardian',
'is_primary' => 0,
'receive_emails' => 1,
'receive_sms' => 0,
],
]);
DB::table('invoices')->insert([
'id' => 1,
'parent_id' => 10,
'invoice_number' => 'INV-1',
'total_amount' => 200.00,
'balance' => 150.00,
'paid_amount' => 50.00,
'status' => 'Partially Paid',
'school_year' => '2024-2025',
'semester' => 'Spring',
'issue_date' => '2025-01-01',
]);
$email = Mockery::mock(EmailService::class);
$email->shouldReceive('send')->twice()->andReturn(true);
$service = new PaymentTestNotificationService($email);
$result = $service->send('primary@example.com', 'no_payment');
$this->assertTrue($result['ok']);
$this->assertDatabaseHas('payment_notification_logs', [
'parent_id' => 10,
'type' => 'no_payment',
'to_email' => 'primary@example.com',
]);
}
}
@@ -0,0 +1,77 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\EmailService;
use App\Services\Payments\PaypalPaymentSyncService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class PaypalPaymentSyncServiceTest extends TestCase
{
use RefreshDatabase;
public function test_sync_applies_payment_and_marks_synced(): void
{
DB::table('configuration')->insert([
['config_key' => 'school_year', 'config_value' => '2024-2025'],
['config_key' => 'semester', 'config_value' => 'Spring'],
]);
DB::table('users')->insert([
'id' => 1,
'school_id' => 123,
'firstname' => 'Parent',
'lastname' => 'User',
'email' => 'parent@example.com',
'status' => 'Active',
]);
DB::table('invoices')->insert([
'id' => 1,
'parent_id' => 1,
'invoice_number' => 'INV-1',
'total_amount' => 100,
'balance' => 100,
'paid_amount' => 0,
'status' => 'Unpaid',
'school_year' => '2024-2025',
'semester' => 'Spring',
'issue_date' => '2025-01-01',
]);
DB::table('paypal_payments')->insert([
'id' => 1,
'parent_school_id' => 123,
'transaction_id' => 'TXN-1',
'status' => 'COMPLETED',
'amount' => 50,
'synced' => 0,
'sync_attempts' => 0,
'created_at' => '2025-01-15 10:00:00',
]);
$email = Mockery::mock(EmailService::class);
$email->shouldReceive('send')->once()->andReturn(true);
$service = new PaypalPaymentSyncService($email);
$result = $service->sync(false, false);
$this->assertSame(1, $result['processed']);
$this->assertDatabaseHas('paypal_payments', [
'id' => 1,
'synced' => 1,
]);
$this->assertDatabaseHas('payments', [
'invoice_id' => 1,
'parent_id' => 1,
'paid_amount' => 50,
]);
$this->assertDatabaseHas('invoices', [
'id' => 1,
'balance' => 50,
]);
}
}
@@ -0,0 +1,102 @@
<?php
namespace Tests\Unit\Services\School;
use App\Services\EmailService;
use App\Services\Notifications\UserNotificationDispatchService;
use App\Services\School\AccountEventService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class AccountEventServiceTest extends TestCase
{
use RefreshDatabase;
public function test_new_account_added_creates_family_links(): void
{
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '5551112222',
'email' => 'parent@example.com',
'address_street' => '123 Main',
'city' => 'City',
'state' => 'ST',
'zip' => '12345',
'accept_school_policy' => 1,
'is_verified' => 1,
'status' => 'Active',
'is_suspended' => 0,
'failed_attempts' => 0,
'password' => bcrypt('secret'),
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('students')->insert([
'id' => 5,
'school_id' => 1,
'parent_id' => 1,
'firstname' => 'Student',
'lastname' => 'One',
'dob' => '2015-01-01',
'age' => 10,
'gender' => 'Male',
'is_active' => 1,
'photo_consent' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
$email = Mockery::mock(EmailService::class);
$email->shouldReceive('send')->twice()->andReturn(true);
$notifier = Mockery::mock(UserNotificationDispatchService::class);
$service = new AccountEventService($email, $notifier);
$result = $service->newAccountAdded([
'id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'email' => 'parent@example.com',
'address_street' => '123 Main',
'city' => 'City',
'state' => 'ST',
'zip' => '12345',
'cellphone' => '5551112222',
]);
$this->assertTrue($result['ok']);
$this->assertDatabaseHas('families', [
'family_code' => 'FAM-1',
]);
$this->assertDatabaseHas('family_guardians', [
'user_id' => 1,
'is_primary' => 1,
]);
$this->assertDatabaseHas('family_students', [
'student_id' => 5,
]);
}
public function test_delete_unverified_user_handles_missing_admin_emails(): void
{
$email = Mockery::mock(EmailService::class);
$email->shouldReceive('send')->andReturn(true);
$notifier = Mockery::mock(UserNotificationDispatchService::class);
$notifier->shouldReceive('notifyUser')->once();
$service = new AccountEventService($email, $notifier);
$result = $service->deleteUnverifiedUser([
'id' => 2,
'email' => 'inactive@example.com',
]);
$this->assertTrue($result['ok']);
}
}
@@ -0,0 +1,49 @@
<?php
namespace Tests\Unit\Services\School;
use App\Services\EmailService;
use App\Services\Notifications\UserNotificationDispatchService;
use App\Services\School\EnrollmentEventService;
use Mockery;
use Tests\TestCase;
class EnrollmentEventServiceTest extends TestCase
{
public function test_admission_under_review_sends_email(): void
{
$email = Mockery::mock(EmailService::class);
$email->shouldReceive('send')->once()->andReturn(true);
$notifier = Mockery::mock(UserNotificationDispatchService::class);
$notifier->shouldReceive('notifyUser')->once();
$service = new EnrollmentEventService($email, $notifier);
$result = $service->admissionUnderReview([
'user_id' => 1,
'email' => 'parent@example.com',
'firstname' => 'Parent',
'lastname' => 'User',
], [
['firstname' => 'Student', 'lastname' => 'One'],
]);
$this->assertTrue($result['ok']);
}
public function test_admission_under_review_requires_email(): void
{
$email = Mockery::mock(EmailService::class);
$notifier = Mockery::mock(UserNotificationDispatchService::class);
$notifier->shouldReceive('notifyUser')->once();
$service = new EnrollmentEventService($email, $notifier);
$result = $service->admissionUnderReview([
'user_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
], []);
$this->assertFalse($result['ok']);
}
}
@@ -0,0 +1,61 @@
<?php
namespace Tests\Unit\Services\School;
use App\Services\EmailService;
use App\Services\Notifications\UserNotificationDispatchService;
use App\Services\School\PaymentEventService;
use Mockery;
use Tests\TestCase;
class PaymentEventServiceTest extends TestCase
{
public function test_payment_received_sends_email(): void
{
$email = Mockery::mock(EmailService::class);
$email->shouldReceive('send')->once()->andReturn(true);
$notifier = Mockery::mock(UserNotificationDispatchService::class);
$notifier->shouldReceive('notifyUser')->once();
$service = new PaymentEventService($email, $notifier);
$result = $service->paymentReceived([
'user_id' => 1,
'email' => 'parent@example.com',
'firstname' => 'Parent',
'lastname' => 'User',
'amount' => 25,
'invoice_id' => 10,
'payment_date' => now()->toDateTimeString(),
'installment_seq' => 1,
'pre_balance' => 50,
'post_balance' => 25,
'invoice_total' => 100,
]);
$this->assertTrue($result['ok']);
}
public function test_extra_charge_requires_email(): void
{
$email = Mockery::mock(EmailService::class);
$notifier = Mockery::mock(UserNotificationDispatchService::class);
$notifier->shouldReceive('notifyUser')->once();
$service = new PaymentEventService($email, $notifier);
$result = $service->extraCharge([
'user_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'amount_signed' => 10,
'amount_abs' => 10,
'pre_balance' => 0,
'post_balance' => 10,
'invoice_total' => 100,
'charge_title' => 'Fee',
'charge_type' => 'add',
]);
$this->assertFalse($result['ok']);
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tests\Unit\Services\Staff;
use App\Services\Staff\StaffTimeOffLinkService;
use Tests\TestCase;
class StaffTimeOffLinkServiceTest extends TestCase
{
public function test_token_round_trip(): void
{
$service = new StaffTimeOffLinkService('secret', 3600);
$token = $service->createToken(['uid' => 10, 'email' => 'staff@example.com']);
$payload = $service->parseToken($token);
$this->assertNotNull($payload);
$this->assertSame(10, $payload['uid']);
$this->assertSame('staff@example.com', $payload['email']);
}
public function test_token_invalid_on_expiry(): void
{
$service = new StaffTimeOffLinkService('secret', 1);
$token = $service->createToken(['uid' => 10]);
sleep(2);
$payload = $service->parseToken($token);
$this->assertNull($payload);
}
}
@@ -0,0 +1,33 @@
<?php
namespace Tests\Unit\Services\System;
use App\Services\System\ConfigUpdateService;
use DateTimeZone;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ConfigUpdateServiceTest extends TestCase
{
use RefreshDatabase;
public function test_run_task_rejects_unknown_task(): void
{
$service = new ConfigUpdateService();
$result = $service->runTask('unknown', false, false, new DateTimeZone('UTC'));
$this->assertFalse($result['ok']);
}
public function test_run_task_sets_config_when_forced(): void
{
$service = new ConfigUpdateService();
$result = $service->runTask('enable_attendance_on', false, true, new DateTimeZone('UTC'));
$this->assertTrue($result['ok']);
$this->assertDatabaseHas('configuration', [
'config_key' => 'enable_attendance',
'config_value' => '1',
]);
}
}
@@ -2,7 +2,7 @@
namespace Tests\Unit\Services\Teachers;
use App\Libraries\StaffTimeOffLinkService;
use App\Services\Staff\StaffTimeOffLinkService;
use App\Services\SemesterRangeService;
use App\Services\Semesters\SemesterConfigService;
use App\Services\Teachers\TeacherAbsenceService;
@@ -0,0 +1,51 @@
<?php
namespace Tests\Unit\Services\Users;
use App\Services\Users\InactiveUserCleanupService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class InactiveUserCleanupServiceTest extends TestCase
{
use RefreshDatabase;
public function test_cleanup_removes_inactive_users_and_roles(): void
{
DB::table('users')->insert([
'id' => 1,
'firstname' => 'Inactive',
'lastname' => 'User',
'email' => 'inactive@example.com',
'status' => 'Inactive',
'created_at' => now()->subMinutes(30),
]);
DB::table('parents')->insert([
'id' => 1,
'secondparent_firstname' => 'Second',
'secondparent_lastname' => 'Parent',
'secondparent_gender' => 'Female',
'secondparent_email' => 'second@example.com',
'secondparent_phone' => '1234567890',
'firstparent_id' => 1,
'secondparent_id' => 2,
'semester' => 'Spring',
'school_year' => '2024-2025',
]);
DB::table('user_roles')->insert([
'user_id' => 1,
'role_id' => 1,
]);
$service = new InactiveUserCleanupService();
$result = $service->cleanup(15);
$this->assertSame(1, $result['deleted_users']);
$this->assertDatabaseMissing('users', ['id' => 1]);
$this->assertDatabaseMissing('parents', ['firstparent_id' => 1]);
$this->assertDatabaseMissing('user_roles', ['user_id' => 1]);
}
}
@@ -0,0 +1,43 @@
<?php
namespace Tests\Unit\Services\Whatsapp;
use App\Services\EmailService;
use App\Services\Whatsapp\WhatsappInviteEmailService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class WhatsappInviteEmailServiceTest extends TestCase
{
use RefreshDatabase;
public function test_send_requires_recipients(): void
{
$service = new WhatsappInviteEmailService(Mockery::mock(EmailService::class));
$result = $service->send([]);
$this->assertFalse($result['ok']);
}
public function test_send_dispatches_email(): void
{
$email = Mockery::mock(EmailService::class);
$email->shouldReceive('send')->once()->andReturn(true);
$service = new WhatsappInviteEmailService($email);
$result = $service->send([
'to_emails' => ['parent@example.com'],
'sections' => [
[
'class_section_id' => 1,
'class_section_name' => 'Class A',
'invite_link' => 'https://chat.whatsapp.com/test',
],
],
]);
$this->assertTrue($result['ok']);
$this->assertSame(1, $result['recipients']);
}
}