add controllers, servoices

This commit is contained in:
root
2026-03-09 02:52:13 -04:00
parent c8de5f7edc
commit d76c871cb7
501 changed files with 34439 additions and 21843 deletions
@@ -2,120 +2,54 @@
namespace Tests\Unit\Services\Administrator;
use App\Models\AdminNotificationSubject;
use App\Services\Administrator\AdminNotificationSubjectService;
use App\Services\Administrator\AdminNotificationUserService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class AdminNotificationSubjectServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_alerts_data_includes_admins(): void
{
Mockery::close();
parent::tearDown();
}
public function test_alerts_data_returns_expected_payload_when_table_exists(): void
{
DB::shouldReceive('getSchemaBuilder->hasTable')
->once()
->with('admin_notification_subjects')
->andReturn(true);
$userService = Mockery::mock(AdminNotificationUserService::class);
$userService->shouldReceive('fetchAdminNotificationUsers')
->once()
->andReturn([
['id' => 1, 'firstname' => 'Admin', 'lastname' => 'One', 'email' => 'a1@test.com'],
['id' => 2, 'firstname' => 'Admin', 'lastname' => 'Two', 'email' => 'a2@test.com'],
]);
$row1 = (object) ['id' => 10, 'admin_id' => 1, 'subject' => 'academics'];
$row2 = (object) ['id' => 11, 'admin_id' => 1, 'subject' => 'finance'];
$row3 = (object) ['id' => 12, 'admin_id' => 2, 'subject' => 'general'];
AdminNotificationSubject::shouldReceive('query->select->whereIn->get')
->once()
->andReturn(collect([$row1, $row2, $row3]));
$service = new AdminNotificationSubjectService($userService);
$result = $service->alertsData();
$this->assertTrue($result['tableReady']);
$this->assertCount(2, $result['admins']);
$this->assertArrayHasKey('academics', $result['subjects']);
$this->assertTrue($result['assignedSubjects'][1]['academics']);
$this->assertTrue($result['assignedSubjects'][1]['finance']);
$this->assertTrue($result['assignedSubjects'][2]['general']);
}
public function test_save_returns_error_when_table_missing(): void
{
DB::shouldReceive('getSchemaBuilder->hasTable')
->once()
->with('admin_notification_subjects')
->andReturn(false);
$userService = Mockery::mock(AdminNotificationUserService::class);
$service = new AdminNotificationSubjectService($userService);
$result = $service->save([
1 => ['academics'],
DB::table('roles')->insert([
['id' => 1, 'name' => 'Admin', 'slug' => 'admin', 'dashboard_route' => 'admin', 'priority' => 1, 'is_active' => 1],
]);
$this->assertFalse($result['success']);
$this->assertSame(422, $result['status']);
}
public function test_save_inserts_and_deletes_subjects(): void
{
DB::shouldReceive('getSchemaBuilder->hasTable')
->once()
->with('admin_notification_subjects')
->andReturn(true);
$userService = Mockery::mock(AdminNotificationUserService::class);
$userService->shouldReceive('fetchAdminNotificationUsers')
->once()
->andReturn([
['id' => 1, 'firstname' => 'Admin', 'lastname' => 'One', 'email' => 'a1@test.com'],
['id' => 2, 'firstname' => 'Admin', 'lastname' => 'Two', 'email' => 'a2@test.com'],
]);
$existing1 = (object) ['id' => 101, 'admin_id' => 1, 'subject' => 'academics'];
$existing2 = (object) ['id' => 102, 'admin_id' => 1, 'subject' => 'finance'];
$existing3 = (object) ['id' => 103, 'admin_id' => 2, 'subject' => 'general'];
AdminNotificationSubject::shouldReceive('query->select->whereIn->get')
->once()
->andReturn(collect([$existing1, $existing2, $existing3]));
AdminNotificationSubject::shouldReceive('query->where->whereIn->delete')
->once()
->andReturn(1);
AdminNotificationSubject::shouldReceive('insert')
->once()
->with(Mockery::on(function ($batch) {
return count($batch) === 2
&& $batch[0]['admin_id'] === 1
&& $batch[0]['subject'] === 'general'
&& $batch[1]['admin_id'] === 2
&& $batch[1]['subject'] === 'attendance';
}))
->andReturn(true);
$service = new AdminNotificationSubjectService($userService);
$result = $service->save([
1 => ['academics', 'general'],
2 => ['general', 'attendance'],
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Admin',
'lastname' => 'User',
'cellphone' => '5555555555',
'email' => 'admin@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',
]);
$this->assertTrue($result['success']);
$this->assertSame(200, $result['status']);
DB::table('user_roles')->insert([
'id' => 1,
'user_id' => 1,
'role_id' => 1,
]);
$userService = new \App\Services\Administrator\AdminNotificationUserService();
$service = new AdminNotificationSubjectService($userService);
$data = $service->alertsData();
$this->assertTrue($data['tableReady']);
$this->assertCount(1, $data['admins']);
}
}
}
@@ -0,0 +1,75 @@
<?php
namespace Tests\Unit\Services\Administrator;
use App\Services\Administrator\AdminNotificationUserService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AdminNotificationUserServiceTest extends TestCase
{
use RefreshDatabase;
public function test_fetch_admin_notification_users_excludes_parents(): void
{
DB::table('roles')->insert([
['id' => 1, 'name' => 'Admin', 'slug' => 'admin', 'dashboard_route' => 'admin', 'priority' => 1, 'is_active' => 1],
['id' => 2, 'name' => 'Parent', 'slug' => 'parent', 'dashboard_route' => 'parent', 'priority' => 2, 'is_active' => 1],
]);
DB::table('users')->insert([
[
'id' => 1,
'school_id' => 1,
'firstname' => 'Admin',
'lastname' => 'User',
'cellphone' => '5555555555',
'email' => 'admin@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',
],
[
'id' => 2,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '5555555555',
'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([
['id' => 1, 'user_id' => 1, 'role_id' => 1],
['id' => 2, 'user_id' => 2, 'role_id' => 2],
]);
$service = new AdminNotificationUserService();
$admins = $service->fetchAdminNotificationUsers();
$this->assertCount(1, $admins);
$this->assertSame('admin@example.com', $admins[0]['email']);
}
}
@@ -2,111 +2,57 @@
namespace Tests\Unit\Services\Administrator;
use App\Models\AdminNotificationSubject;
use App\Services\Administrator\AdminNotificationUserService;
use App\Services\Administrator\AdminPrintRecipientService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class AdminPrintRecipientServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_data_returns_admins_and_assigned(): void
{
Mockery::close();
parent::tearDown();
}
public function test_data_returns_assigned_admins(): void
{
DB::shouldReceive('getSchemaBuilder->hasTable')
->once()
->with('admin_notification_subjects')
->andReturn(true);
$userService = Mockery::mock(AdminNotificationUserService::class);
$userService->shouldReceive('fetchAdminNotificationUsers')
->once()
->andReturn([
['id' => 1, 'firstname' => 'A', 'lastname' => 'One', 'email' => 'a1@test.com'],
['id' => 2, 'firstname' => 'A', 'lastname' => 'Two', 'email' => 'a2@test.com'],
]);
$row1 = (object) ['admin_id' => 1];
$row2 = (object) ['admin_id' => 2];
AdminNotificationSubject::shouldReceive('query->select->where->whereIn->get')
->once()
->andReturn(collect([$row1, $row2]));
$service = new AdminPrintRecipientService($userService);
$result = $service->data();
$this->assertTrue($result['tableReady']);
$this->assertTrue($result['assigned'][1]);
$this->assertTrue($result['assigned'][2]);
}
public function test_save_returns_error_when_table_missing(): void
{
DB::shouldReceive('getSchemaBuilder->hasTable')
->once()
->with('admin_notification_subjects')
->andReturn(false);
$userService = Mockery::mock(AdminNotificationUserService::class);
$service = new AdminPrintRecipientService($userService);
$result = $service->save([
1 => 1,
DB::table('roles')->insert([
['id' => 1, 'name' => 'Admin', 'slug' => 'admin', 'dashboard_route' => 'admin', 'priority' => 1, 'is_active' => 1],
]);
$this->assertFalse($result['success']);
$this->assertSame(422, $result['status']);
}
public function test_save_updates_print_recipients(): void
{
DB::shouldReceive('getSchemaBuilder->hasTable')
->once()
->with('admin_notification_subjects')
->andReturn(true);
$userService = Mockery::mock(AdminNotificationUserService::class);
$userService->shouldReceive('fetchAdminNotificationUsers')
->once()
->andReturn([
['id' => 1],
['id' => 2],
['id' => 3],
]);
AdminNotificationSubject::shouldReceive('query->where->whereIn->pluck')
->once()
->andReturn(collect([1, 2]));
AdminNotificationSubject::shouldReceive('query->where->whereIn->delete')
->once()
->andReturn(1);
AdminNotificationSubject::shouldReceive('insert')
->once()
->with(Mockery::on(function ($batch) {
return count($batch) === 1
&& $batch[0]['admin_id'] === 3
&& $batch[0]['subject'] === 'print_requests';
}))
->andReturn(true);
$service = new AdminPrintRecipientService($userService);
$result = $service->save([
1 => 1,
3 => 1,
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Admin',
'lastname' => 'User',
'cellphone' => '5555555555',
'email' => 'admin@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',
]);
$this->assertTrue($result['success']);
$this->assertSame(200, $result['status']);
DB::table('user_roles')->insert([
'id' => 1,
'user_id' => 1,
'role_id' => 1,
]);
DB::table('admin_notification_subjects')->insert([
'id' => 1,
'admin_id' => 1,
'subject' => 'print_requests',
]);
$service = new AdminPrintRecipientService(new \App\Services\Administrator\AdminNotificationUserService());
$data = $service->data();
$this->assertTrue($data['assigned'][1]);
}
}
}
@@ -0,0 +1,36 @@
<?php
namespace Tests\Unit\Services\Administrator;
use App\Services\Administrator\AdministratorAbsenceService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Mockery;
use Tests\TestCase;
class AdministratorAbsenceServiceTest extends TestCase
{
use RefreshDatabase;
public function test_submit_requires_reason(): void
{
$shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class);
$shared->shouldReceive('getSemester')->andReturn('Fall');
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
$shared->shouldReceive('allowedAbsenceDates')->andReturn(['2025-01-05']);
$service = new AdministratorAbsenceService(
$shared,
new \App\Models\User(),
new \App\Models\StaffAttendance(),
new \App\Services\SemesterRangeService(new \App\Services\Semesters\SemesterConfigService()),
Mockery::mock(\App\Libraries\StaffTimeOffLinkService::class)
);
$request = Request::create('/absences', 'POST', ['dates' => ['2025-01-05']]);
$result = $service->submit($request, 1);
$this->assertFalse($result['success']);
$this->assertSame(422, $result['status']);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Tests\Unit\Services\Administrator;
use App\Services\Administrator\AdministratorDashboardService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class AdministratorDashboardServiceTest extends TestCase
{
use RefreshDatabase;
public function test_delegates_to_metrics_and_search(): void
{
$metrics = Mockery::mock(\App\Services\Administrator\AdministratorMetricsService::class);
$metrics->shouldReceive('metrics')->once()->andReturn(['counts' => []]);
$search = Mockery::mock(\App\Services\Administrator\AdministratorUserSearchService::class);
$search->shouldReceive('search')->once()->with('john')->andReturn(['results' => []]);
$service = new AdministratorDashboardService($metrics, $search);
$this->assertSame(['counts' => []], $service->metrics());
$this->assertSame(['results' => []], $service->userSearch('john'));
}
}
@@ -0,0 +1,23 @@
<?php
namespace Tests\Unit\Services\Administrator;
use App\Services\Administrator\AdministratorEnrollmentEventService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class AdministratorEnrollmentEventServiceTest extends TestCase
{
use RefreshDatabase;
public function test_dispatch_grouped_events_with_empty_groups(): void
{
Event::fake();
$service = new AdministratorEnrollmentEventService();
$service->dispatchGroupedEvents([], [], [], '2025-2026');
Event::assertNotDispatched('studentEnrolled');
}
}
@@ -2,15 +2,8 @@
namespace Tests\Unit\Services\Administrator;
use App\Models\ClassSection;
use App\Models\Configuration;
use App\Models\Enrollment;
use App\Models\Student;
use App\Models\StudentClass;
use App\Services\Administrator\AdministratorEnrollmentQueryService;
use App\Services\Administrator\AdministratorSharedService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
@@ -18,253 +11,24 @@ class AdministratorEnrollmentQueryServiceTest extends TestCase
{
use RefreshDatabase;
protected function tearDown(): void
public function test_enrollment_withdrawal_data_returns_structure(): void
{
Mockery::close();
parent::tearDown();
}
protected function makeSharedService(string $schoolYear = '2025-2026', string $semester = 'Fall'): AdministratorSharedService
{
$configuration = Mockery::mock(Configuration::class);
$configuration->shouldReceive('getConfig')
->with('school_year')
->andReturn($schoolYear);
$configuration->shouldReceive('getConfig')
->with('semester')
->andReturn($semester);
return new AdministratorSharedService($configuration);
}
public function test_enrollment_withdrawal_data_returns_students_classes_and_school_years(): void
{
DB::table('enrollments')->insert([
[
'student_id' => 1,
'parent_id' => 100,
'school_year' => '2025-2026',
'semester' => 'Fall',
'enrollment_status' => 'enrolled',
'admission_status' => 'accepted',
'is_withdrawn' => 0,
'created_at' => now(),
'updated_at' => now(),
],
[
'student_id' => 2,
'parent_id' => 101,
'school_year' => '2024-2025',
'semester' => 'Fall',
'enrollment_status' => 'withdrawn',
'admission_status' => 'accepted',
'is_withdrawn' => 1,
'created_at' => now(),
'updated_at' => now(),
],
]);
$studentModel = Mockery::mock(Student::class);
$studentModel->shouldReceive('getStudentsWithClassAndEnrollment')
->once()
->andReturn([
[
'id' => 1,
'firstname' => 'Ali',
'lastname' => 'Ben',
'parent_id' => 100,
'parent_firstname' => 'Parent',
'parent_lastname' => 'One',
'is_new' => 1,
'registration_date' => '2025-09-10 08:00:00',
'admission_status' => 'accepted',
],
[
'id' => 2,
'firstname' => 'Sara',
'lastname' => 'Ali',
'parent_id' => 101,
'parent_firstname' => 'Parent',
'parent_lastname' => 'Two',
'is_new' => 0,
'registration_date' => '2024-09-10 08:00:00',
'admission_status' => 'accepted',
],
]);
$studentClassModel = Mockery::mock(StudentClass::class);
$studentClassModel->shouldReceive('getClassSectionsByStudentId')
->with(1, '2025-2026')
->andReturn('Grade 5A');
$studentClassModel->shouldReceive('getClassSectionsByStudentId')
->with(2, '2025-2026')
->andReturn('');
$enrollmentModel = Mockery::mock(Enrollment::class);
$enrollmentModel->shouldReceive('getEnrollmentStatus')
->with(1, '2025-2026')
->andReturn('enrolled');
$enrollmentModel->shouldReceive('getEnrollmentStatus')
->with(2, '2025-2026')
->andReturn('');
ClassSection::unguard();
ClassSection::create([
'class_section_id' => 10,
'class_section_name' => 'Grade 5A',
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
$shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class);
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
$shared->shouldReceive('getSemester')->andReturn('Fall');
$shared->shouldReceive('getSchoolYearStartYear')->andReturn(2025);
$service = new AdministratorEnrollmentQueryService(
$this->makeSharedService(),
$studentModel,
$studentClassModel,
$enrollmentModel,
new ClassSection()
$shared,
new \App\Models\Student(),
new \App\Models\StudentClass(),
new \App\Models\Enrollment(),
new \App\Models\ClassSection()
);
$result = $service->enrollmentWithdrawalData('2025-2026');
$data = $service->enrollmentWithdrawalData('2025-2026');
$this->assertSame('2025-2026', $result['school_year']);
$this->assertSame('Fall', $result['semester']);
$this->assertNotEmpty($result['schoolYears']);
$this->assertCount(2, $result['students']);
$this->assertCount(1, $result['classes']);
$student1 = collect($result['students'])->firstWhere('student_id', 1);
$student2 = collect($result['students'])->firstWhere('student_id', 2);
$this->assertSame('Yes', $student1['new_student']);
$this->assertSame('Grade 5A', $student1['class_section']);
$this->assertSame('enrolled', $student1['enrollment_status']);
$this->assertSame('2025-09-10', $student1['registration_date_order']);
$this->assertSame('No', $student2['new_student']);
$this->assertSame('Class not Assigned', $student2['class_section']);
$this->assertArrayHasKey('students', $data);
$this->assertArrayHasKey('classes', $data);
}
public function test_enrollment_withdrawal_data_marks_removed_previous_year(): void
{
DB::table('enrollments')->insert([
[
'student_id' => 5,
'parent_id' => 200,
'school_year' => '2024-2025',
'semester' => 'Fall',
'enrollment_status' => 'withdrawn',
'admission_status' => 'accepted',
'is_withdrawn' => 1,
'created_at' => now(),
'updated_at' => now(),
],
]);
$studentModel = Mockery::mock(Student::class);
$studentModel->shouldReceive('getStudentsWithClassAndEnrollment')
->once()
->andReturn([
[
'id' => 5,
'firstname' => 'Mina',
'lastname' => 'K',
'parent_id' => 200,
'parent_firstname' => 'Parent',
'parent_lastname' => 'Three',
'is_new' => 0,
'registration_date' => '2024-08-01 00:00:00',
'admission_status' => 'accepted',
],
]);
$studentClassModel = Mockery::mock(StudentClass::class);
$studentClassModel->shouldReceive('getClassSectionsByStudentId')
->with(5, '2025-2026')
->andReturn('Grade 6A');
$enrollmentModel = Mockery::mock(Enrollment::class);
$enrollmentModel->shouldReceive('getEnrollmentStatus')
->with(5, '2025-2026')
->andReturn('payment pending');
$service = new AdministratorEnrollmentQueryService(
$this->makeSharedService('2025-2026', 'Fall'),
$studentModel,
$studentClassModel,
$enrollmentModel,
new ClassSection()
);
$result = $service->enrollmentWithdrawalData('2025-2026');
$student = $result['students'][0];
$this->assertSame('Yes', $student['removed_previous_year']);
$this->assertSame('payment pending', $student['enrollment_status']);
}
public function test_new_students_data_returns_transformed_rows(): void
{
$studentModel = Mockery::mock(Student::class);
$studentModel->shouldReceive('getStudentsWithParentsAndEmergency')
->once()
->with('2025-2026')
->andReturn([
[
'id' => 9,
'firstname' => 'Adam',
'lastname' => 'Y',
'is_new' => 1,
'registration_date' => '2025-10-01 10:00:00',
],
[
'id' => 10,
'firstname' => 'Eve',
'lastname' => 'Z',
'is_new' => 0,
'registration_date' => null,
],
]);
$studentClassModel = Mockery::mock(StudentClass::class);
$studentClassModel->shouldReceive('getClassSectionsByStudentId')
->with(9, '2025-2026')
->andReturn('Grade 1A');
$studentClassModel->shouldReceive('getClassSectionsByStudentId')
->with(10, '2025-2026')
->andReturn('');
$enrollmentModel = Mockery::mock(Enrollment::class);
$enrollmentModel->shouldReceive('getEnrollmentStatus')
->with(9, '2025-2026')
->andReturn('enrolled');
$enrollmentModel->shouldReceive('getEnrollmentStatus')
->with(10, '2025-2026')
->andReturn('waitlist');
$service = new AdministratorEnrollmentQueryService(
$this->makeSharedService(),
$studentModel,
$studentClassModel,
$enrollmentModel,
new ClassSection()
);
$result = $service->newStudentsData();
$this->assertSame(2, $result['total_new']);
$first = $result['new_students'][0];
$second = $result['new_students'][1];
$this->assertSame('Yes', $first['new_student']);
$this->assertSame('Grade 1A', $first['class_section']);
$this->assertSame('enrolled', $first['enrollment_status']);
$this->assertSame('contact_9', $first['modalIdContact']);
$this->assertSame('2025-10-01', $first['registration_date']);
$this->assertSame('No', $second['new_student']);
$this->assertSame('Class not Assigned', $second['class_section']);
$this->assertSame('waitlist', $second['enrollment_status']);
}
}
}
@@ -0,0 +1,34 @@
<?php
namespace Tests\Unit\Services\Administrator;
use App\Services\Administrator\AdministratorEnrollmentRefundService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class AdministratorEnrollmentRefundServiceTest extends TestCase
{
use RefreshDatabase;
public function test_process_refunds_reports_missing_invoice(): void
{
DB::table('enrollments')->insert([
'id' => 1,
'student_id' => 1,
'parent_id' => 10,
'enrollment_date' => '2025-01-01',
'enrollment_status' => 'withdrawn',
'is_withdrawn' => 1,
'school_year' => '2025-2026',
]);
$feeCalc = Mockery::mock(\App\Services\FeeCalculationService::class);
$service = new AdministratorEnrollmentRefundService($feeCalc);
$result = $service->processRefunds([10], '2025-2026', 1);
$this->assertNotEmpty($result['errors']);
}
}
@@ -0,0 +1,29 @@
<?php
namespace Tests\Unit\Services\Administrator;
use App\Services\Administrator\AdministratorEnrollmentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class AdministratorEnrollmentServiceTest extends TestCase
{
use RefreshDatabase;
public function test_delegates_to_query_and_status_services(): void
{
$query = Mockery::mock(\App\Services\Administrator\AdministratorEnrollmentQueryService::class);
$query->shouldReceive('enrollmentWithdrawalData')->once()->andReturn(['rows' => []]);
$query->shouldReceive('newStudentsData')->once()->andReturn(['students' => []]);
$status = Mockery::mock(\App\Services\Administrator\AdministratorEnrollmentStatusService::class);
$status->shouldReceive('updateStatuses')->once()->andReturn(['success' => true]);
$service = new AdministratorEnrollmentService($query, $status);
$this->assertSame(['rows' => []], $service->enrollmentWithdrawalData());
$this->assertSame(['students' => []], $service->showNewStudentsData());
$this->assertSame(['success' => true], $service->updateStatuses([], 1));
}
}
@@ -2,236 +2,35 @@
namespace Tests\Unit\Services\Administrator;
use App\Models\Student;
use App\Models\User;
use App\Services\Administrator\AdministratorEnrollmentEventService;
use App\Services\Administrator\AdministratorEnrollmentRefundService;
use App\Services\Administrator\AdministratorEnrollmentStatusService;
use App\Services\Administrator\AdministratorSharedService;
use Illuminate\Support\Facades\DB;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class AdministratorEnrollmentStatusServiceTest extends TestCase
{
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
use RefreshDatabase;
public function test_update_statuses_returns_validation_error_when_empty(): void
public function test_update_statuses_requires_payload(): void
{
$shared = Mockery::mock(AdministratorSharedService::class);
$studentModel = Mockery::mock(Student::class);
$userModel = Mockery::mock(User::class);
$refundService = Mockery::mock(AdministratorEnrollmentRefundService::class);
$eventService = Mockery::mock(AdministratorEnrollmentEventService::class);
$shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class);
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
$shared->shouldReceive('getSemester')->andReturn('Fall');
$refundService = Mockery::mock(\App\Services\Administrator\AdministratorEnrollmentRefundService::class);
$eventService = Mockery::mock(\App\Services\Administrator\AdministratorEnrollmentEventService::class);
$service = new AdministratorEnrollmentStatusService(
$shared,
$studentModel,
$userModel,
new \App\Models\Student(),
new \App\Models\User(),
$refundService,
$eventService
);
$result = $service->updateStatuses([], 99);
$result = $service->updateStatuses([], 1);
$this->assertFalse($result['success']);
$this->assertSame(422, $result['status']);
}
public function test_update_statuses_creates_new_enrollment_and_dispatches_events(): void
{
$shared = Mockery::mock(AdministratorSharedService::class);
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
$shared->shouldReceive('getSemester')->andReturn('Fall');
$studentModel = Mockery::mock(Student::class);
$studentModel->shouldReceive('find')
->once()
->with(10)
->andReturn([
'id' => 10,
'firstname' => 'Ali',
'lastname' => 'Ben',
'parent_id' => 20,
]);
$userModel = Mockery::mock(User::class);
$userModel->shouldReceive('find')
->once()
->with(20)
->andReturn([
'id' => 20,
'firstname' => 'Parent',
'lastname' => 'One',
'email' => 'parent@test.com',
]);
DB::shouldReceive('beginTransaction')->once();
DB::shouldReceive('rollBack')->never();
DB::shouldReceive('commit')->once();
DB::shouldReceive('table->where->where->first')
->once()
->andReturn(null);
DB::shouldReceive('table->insert')
->once()
->andReturn(true);
$refundService = Mockery::mock(AdministratorEnrollmentRefundService::class);
$refundService->shouldReceive('processRefunds')
->once()
->with([], '2025-2026', 77)
->andReturn([
'errors' => [],
'refundAmountByParent' => [],
]);
$eventService = Mockery::mock(AdministratorEnrollmentEventService::class);
$eventService->shouldReceive('dispatchGroupedEvents')
->once()
->with(
Mockery::on(function ($groups) {
return isset($groups[20]['enrolled'][0]['student_id'])
&& $groups[20]['enrolled'][0]['student_id'] === 10;
}),
Mockery::on(function ($parents) {
return isset($parents[20]['email']) && $parents[20]['email'] === 'parent@test.com';
}),
[],
'2025-2026'
);
$service = new AdministratorEnrollmentStatusService(
$shared,
$studentModel,
$userModel,
$refundService,
$eventService
);
$result = $service->updateStatuses([
10 => 'enrolled',
], 77);
$this->assertTrue($result['success']);
$this->assertSame(200, $result['status']);
}
public function test_update_statuses_updates_existing_enrollment_and_processes_refund(): void
{
$shared = Mockery::mock(AdministratorSharedService::class);
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
$shared->shouldReceive('getSemester')->andReturn('Fall');
$studentModel = Mockery::mock(Student::class);
$studentModel->shouldReceive('find')
->once()
->with(10)
->andReturn([
'id' => 10,
'firstname' => 'Sara',
'lastname' => 'Ali',
'parent_id' => 20,
]);
$userModel = Mockery::mock(User::class);
$userModel->shouldReceive('find')
->once()
->with(20)
->andReturn([
'id' => 20,
'firstname' => 'Parent',
'lastname' => 'Two',
'email' => 'p2@test.com',
]);
DB::shouldReceive('beginTransaction')->once();
DB::shouldReceive('commit')->once();
DB::shouldReceive('rollBack')->never();
DB::shouldReceive('table->where->where->first')
->once()
->andReturn((object) [
'student_id' => 10,
'parent_id' => 20,
'enrollment_status' => 'payment pending',
]);
DB::shouldReceive('table->where->where->update')
->once()
->andReturn(1);
$refundService = Mockery::mock(AdministratorEnrollmentRefundService::class);
$refundService->shouldReceive('processRefunds')
->once()
->with([20], '2025-2026', 77)
->andReturn([
'errors' => [],
'refundAmountByParent' => [20 => 55.50],
]);
$eventService = Mockery::mock(AdministratorEnrollmentEventService::class);
$eventService->shouldReceive('dispatchGroupedEvents')
->once();
$service = new AdministratorEnrollmentStatusService(
$shared,
$studentModel,
$userModel,
$refundService,
$eventService
);
$result = $service->updateStatuses([
10 => 'refund pending',
], 77);
$this->assertTrue($result['success']);
$this->assertSame(200, $result['status']);
}
public function test_update_statuses_returns_partial_success_when_invalid_status_present(): void
{
$shared = Mockery::mock(AdministratorSharedService::class);
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
$shared->shouldReceive('getSemester')->andReturn('Fall');
DB::shouldReceive('beginTransaction')->once();
DB::shouldReceive('commit')->once();
$studentModel = Mockery::mock(Student::class);
$userModel = Mockery::mock(User::class);
$refundService = Mockery::mock(AdministratorEnrollmentRefundService::class);
$refundService->shouldReceive('processRefunds')
->once()
->andReturn([
'errors' => [],
'refundAmountByParent' => [],
]);
$eventService = Mockery::mock(AdministratorEnrollmentEventService::class);
$eventService->shouldReceive('dispatchGroupedEvents')->once();
$service = new AdministratorEnrollmentStatusService(
$shared,
$studentModel,
$userModel,
$refundService,
$eventService
);
$result = $service->updateStatuses([
10 => 'not-valid',
], 77);
$this->assertFalse($result['success']);
$this->assertSame(207, $result['status']);
$this->assertStringContainsString('Invalid enrollment status', $result['message']);
}
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tests\Unit\Services\Administrator;
use App\Services\Administrator\AdministratorMetricsService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AdministratorMetricsServiceTest extends TestCase
{
use RefreshDatabase;
public function test_metrics_returns_counts(): void
{
DB::table('configuration')->insert([
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
]);
$service = new AdministratorMetricsService(
new \App\Services\Administrator\AdministratorSharedService(new \App\Models\Configuration()),
new \App\Models\User(),
new \App\Models\LoginActivity()
);
$metrics = $service->metrics();
$this->assertSame(0, $metrics['counts']['students']);
$this->assertSame('2025-2026', $metrics['meta']['schoolYear']);
}
}
@@ -0,0 +1,31 @@
<?php
namespace Tests\Unit\Services\Administrator;
use App\Services\Administrator\AdministratorNotificationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class AdministratorNotificationServiceTest extends TestCase
{
use RefreshDatabase;
public function test_delegates_to_subject_and_print_services(): void
{
$subject = Mockery::mock(\App\Services\Administrator\AdminNotificationSubjectService::class);
$subject->shouldReceive('alertsData')->once()->andReturn(['admins' => []]);
$subject->shouldReceive('save')->once()->andReturn(['success' => true]);
$print = Mockery::mock(\App\Services\Administrator\AdminPrintRecipientService::class);
$print->shouldReceive('data')->once()->andReturn(['admins' => []]);
$print->shouldReceive('save')->once()->andReturn(['success' => true]);
$service = new AdministratorNotificationService($subject, $print);
$this->assertSame(['admins' => []], $service->notificationsAlertsData());
$this->assertSame(['success' => true], $service->saveNotificationSubjects([]));
$this->assertSame(['admins' => []], $service->printNotificationRecipientsData());
$this->assertSame(['success' => true], $service->savePrintNotificationRecipients([]));
}
}
@@ -0,0 +1,21 @@
<?php
namespace Tests\Unit\Services\Administrator;
use App\Services\Administrator\AdministratorSharedService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdministratorSharedServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_previous_school_year(): void
{
$service = new AdministratorSharedService(new \App\Models\Configuration());
$this->assertSame('2024-2025', $service->getPreviousSchoolYear('2025-2026'));
$this->assertSame('2024-25', $service->getPreviousSchoolYear('2025-26'));
$this->assertSame('2024', $service->getPreviousSchoolYear('2025'));
}
}
@@ -0,0 +1,27 @@
<?php
namespace Tests\Unit\Services\Administrator;
use App\Services\Administrator\AdministratorTeacherSubmissionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class AdministratorTeacherSubmissionServiceTest extends TestCase
{
use RefreshDatabase;
public function test_delegates_to_report_and_notification(): void
{
$report = Mockery::mock(\App\Services\Administrator\TeacherSubmissionReportService::class);
$report->shouldReceive('report')->once()->andReturn(['rows' => []]);
$notify = Mockery::mock(\App\Services\Administrator\TeacherSubmissionNotificationService::class);
$notify->shouldReceive('send')->once()->andReturn(['success' => true]);
$service = new AdministratorTeacherSubmissionService($report, $notify);
$this->assertSame(['rows' => []], $service->report());
$this->assertSame(['success' => true], $service->sendNotifications(new \Illuminate\Http\Request(), 1));
}
}
@@ -0,0 +1,22 @@
<?php
namespace Tests\Unit\Services\Administrator;
use App\Services\Administrator\AdministratorUserSearchService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdministratorUserSearchServiceTest extends TestCase
{
use RefreshDatabase;
public function test_empty_query_returns_no_results(): void
{
$service = new AdministratorUserSearchService();
$result = $service->search('');
$this->assertSame('', $result['query']);
$this->assertSame([], $result['results']);
$this->assertSame(0, $result['total_found']);
}
}
@@ -2,172 +2,27 @@
namespace Tests\Unit\Services\Administrator;
use App\Models\ClassSection;
use App\Models\TeacherSubmissionNotificationHistory;
use App\Models\User;
use App\Services\Administrator\AdministratorSharedService;
use App\Services\Administrator\TeacherSubmissionNotificationService;
use App\Services\Administrator\TeacherSubmissionSupportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Mockery;
use Tests\TestCase;
class TeacherSubmissionNotificationServiceTest extends TestCase
{
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
use RefreshDatabase;
public function test_send_returns_validation_style_error_when_notify_missing(): void
public function test_send_requires_targets(): void
{
$shared = Mockery::mock(AdministratorSharedService::class);
$support = new TeacherSubmissionSupportService();
$shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class);
$support = Mockery::mock(\App\Services\Administrator\TeacherSubmissionSupportService::class);
$service = new TeacherSubmissionNotificationService($shared, $support);
$request = new Request([]);
$request = Request::create('/notify', 'POST', []);
$result = $service->send($request, 1);
$this->assertFalse($result['success']);
$this->assertSame(422, $result['status']);
}
public function test_send_sends_email_and_logs_history(): void
{
$shared = Mockery::mock(AdministratorSharedService::class);
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
$shared->shouldReceive('getSemester')->andReturn('Fall');
$support = new TeacherSubmissionSupportService();
$request = new Request([
'notify' => [
1 => [
5 => 1,
],
],
'missing_items' => [
1 => [
5 => base64_encode(json_encode(['midterm scores', 'attendance'])),
],
],
]);
$section = new ClassSection();
$section->class_section_id = 1;
$section->class_section_name = 'Grade 5A';
$teacher = new User();
$teacher->id = 5;
$teacher->firstname = 'John';
$teacher->lastname = 'Teacher';
$teacher->email = 'teacher@test.com';
$admin = new User();
$admin->id = 1;
$admin->firstname = 'Admin';
$admin->lastname = 'User';
ClassSection::shouldReceive('query->select->whereIn->get->keyBy')
->once()
->andReturn(collect([1 => $section]));
User::shouldReceive('query->select->whereIn->get->keyBy')
->once()
->andReturn(collect([5 => $teacher]));
User::shouldReceive('find')
->once()
->with(1)
->andReturn($admin);
Mail::fake();
TeacherSubmissionNotificationHistory::shouldReceive('create')
->once()
->with(Mockery::on(function ($data) {
return $data['teacher_id'] === 5
&& $data['class_section_id'] === 1
&& $data['admin_id'] === 1
&& $data['notification_category'] === 'teacher_submissions'
&& $data['status'] === 'sent';
}))
->andReturnTrue();
$service = new TeacherSubmissionNotificationService($shared, $support);
$result = $service->send($request, 1);
$this->assertTrue($result['success']);
$this->assertSame(200, $result['status']);
$this->assertSame(1, $result['sent']);
$this->assertSame(0, $result['failed']);
Mail::assertSentCount(1);
}
public function test_send_marks_failed_when_teacher_email_invalid(): void
{
$shared = Mockery::mock(AdministratorSharedService::class);
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
$shared->shouldReceive('getSemester')->andReturn('Fall');
$support = new TeacherSubmissionSupportService();
$request = new Request([
'notify' => [
1 => [
5 => 1,
],
],
]);
$section = new ClassSection();
$section->class_section_id = 1;
$section->class_section_name = 'Grade 5A';
$teacher = new User();
$teacher->id = 5;
$teacher->firstname = 'John';
$teacher->lastname = 'Teacher';
$teacher->email = 'invalid-email';
$admin = new User();
$admin->id = 1;
$admin->firstname = 'Admin';
$admin->lastname = 'User';
ClassSection::shouldReceive('query->select->whereIn->get->keyBy')
->once()
->andReturn(collect([1 => $section]));
User::shouldReceive('query->select->whereIn->get->keyBy')
->once()
->andReturn(collect([5 => $teacher]));
User::shouldReceive('find')
->once()
->with(1)
->andReturn($admin);
TeacherSubmissionNotificationHistory::shouldReceive('create')
->once()
->with(Mockery::on(function ($data) {
return $data['status'] === 'failed';
}))
->andReturnTrue();
$service = new TeacherSubmissionNotificationService($shared, $support);
$result = $service->send($request, 1);
$this->assertFalse($result['success']);
$this->assertSame(207, $result['status']);
$this->assertSame(0, $result['sent']);
$this->assertSame(1, $result['failed']);
}
}
}
@@ -2,16 +2,8 @@
namespace Tests\Unit\Services\Administrator;
use App\Models\AttendanceDay;
use App\Models\Configuration;
use App\Models\ScoreComment;
use App\Models\SemesterScore;
use App\Models\TeacherSubmissionNotificationHistory;
use App\Services\Administrator\AdministratorSharedService;
use App\Services\Administrator\TeacherSubmissionReportService;
use App\Services\Administrator\TeacherSubmissionSupportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
@@ -19,298 +11,31 @@ class TeacherSubmissionReportServiceTest extends TestCase
{
use RefreshDatabase;
protected function tearDown(): void
public function test_report_returns_summary(): void
{
Mockery::close();
parent::tearDown();
$shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class);
$shared->shouldReceive('getSemester')->andReturn('Fall');
$shared->shouldReceive('getSchoolYear')->andReturn('2025-2026');
$support = Mockery::mock(\App\Services\Administrator\TeacherSubmissionSupportService::class);
$support->shouldReceive('submissionStatus')->andReturn([
'label' => 'No students',
'badge' => 'bg-secondary',
'detail' => '',
'completed' => true,
]);
$support->shouldReceive('attendanceStatus')->andReturn([
'label' => 'Missing',
'badge' => 'bg-danger',
'completed' => false,
]);
$support->shouldReceive('buildMissingItems')->andReturn([]);
$support->shouldReceive('teacherRolePriority')->andReturn(1);
$service = new TeacherSubmissionReportService($shared, $support);
$report = $service->report();
$this->assertArrayHasKey('summary', $report);
$this->assertSame('2025-2026', $report['schoolYear']);
}
protected function makeSharedService(string $schoolYear = '2025-2026', string $semester = 'Fall'): AdministratorSharedService
{
$configuration = Mockery::mock(Configuration::class);
$configuration->shouldReceive('getConfig')
->with('school_year')
->andReturn($schoolYear);
$configuration->shouldReceive('getConfig')
->with('semester')
->andReturn($semester);
return new AdministratorSharedService($configuration);
}
public function test_report_returns_rows_summary_and_history(): void
{
DB::table('teacher_class')->insert([
[
'class_section_id' => 10,
'teacher_id' => 100,
'position' => 'main',
'school_year' => '2025-2026',
'semester' => 'Fall',
],
[
'class_section_id' => 10,
'teacher_id' => 101,
'position' => 'ta',
'school_year' => '2025-2026',
'semester' => 'Fall',
],
]);
DB::table('classSection')->insert([
[
'class_section_id' => 10,
'class_section_name' => 'Grade 5A',
],
]);
DB::table('users')->insert([
[
'id' => 100,
'firstname' => 'Main',
'lastname' => 'Teacher',
'email' => 'main@test.com',
],
[
'id' => 101,
'firstname' => 'TA',
'lastname' => 'Teacher',
'email' => 'ta@test.com',
],
[
'id' => 900,
'firstname' => 'Admin',
'lastname' => 'One',
'email' => 'admin@test.com',
],
]);
DB::table('student_class')->insert([
[
'student_id' => 1,
'class_section_id' => 10,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
[
'student_id' => 2,
'class_section_id' => 10,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
]);
SemesterScore::unguard();
SemesterScore::create([
'student_id' => 1,
'class_section_id' => 10,
'semester' => 'Fall',
'school_year' => '2025-2026',
'midterm_exam_score' => '88',
'participation_score' => '10',
]);
SemesterScore::create([
'student_id' => 2,
'class_section_id' => 10,
'semester' => 'Fall',
'school_year' => '2025-2026',
'midterm_exam_score' => '',
'participation_score' => '9',
]);
ScoreComment::unguard();
ScoreComment::create([
'student_id' => 1,
'semester' => 'Fall',
'school_year' => '2025-2026',
'score_type' => 'midterm',
'comment' => 'Good progress',
]);
ScoreComment::create([
'student_id' => 1,
'semester' => 'Fall',
'school_year' => '2025-2026',
'score_type' => 'ptap',
'comment' => 'Great effort',
]);
AttendanceDay::unguard();
AttendanceDay::create([
'class_section_id' => 10,
'semester' => 'Fall',
'school_year' => '2025-2026',
'date' => now()->format('Y-m-d'),
'status' => 'submitted',
]);
TeacherSubmissionNotificationHistory::unguard();
TeacherSubmissionNotificationHistory::create([
'teacher_id' => 100,
'class_section_id' => 10,
'admin_id' => 900,
'notification_category' => 'teacher_submissions',
'message' => 'Reminder sent',
'status' => 'sent',
'school_year' => '2025-2026',
'semester' => 'Fall',
'sent_at' => now(),
]);
$service = new TeacherSubmissionReportService(
$this->makeSharedService(),
new TeacherSubmissionSupportService()
);
$result = $service->report();
$this->assertSame('Fall', $result['semester']);
$this->assertSame('2025-2026', $result['schoolYear']);
$this->assertCount(1, $result['rows']);
$row = $result['rows'][0];
$this->assertSame('Grade 5A', $row['class_section']);
$this->assertSame(10, $row['class_section_id']);
$this->assertCount(2, $row['teachers']);
$this->assertSame('Main: Main Teacher', $row['teachers'][0]['label']);
$this->assertSame('TA: TA Teacher', $row['teachers'][1]['label']);
$this->assertSame('Missing', $row['midterm_score_status']['label']);
$this->assertSame('1/2', $row['midterm_score_status']['detail']);
$this->assertSame('Missing', $row['midterm_comment_status']['label']);
$this->assertSame('1/2', $row['midterm_comment_status']['detail']);
$this->assertSame('Submitted', $row['participation_status']['label']);
$this->assertSame('2/2', $row['participation_status']['detail']);
$this->assertSame('Missing', $row['ptap_comment_status']['label']);
$this->assertSame('1/2', $row['ptap_comment_status']['detail']);
$this->assertSame('Submitted', $row['attendance_status']['label']);
$this->assertSame([
'midterm scores',
'midterm comments',
'PTAP comments',
], $row['missing_items']);
$this->assertSame(2, $row['student_count']);
$this->assertArrayHasKey(10, $result['notificationHistory']);
$this->assertArrayHasKey(100, $result['notificationHistory'][10]);
$this->assertSame('Admin One', $result['notificationHistory'][10][100][0]['admin_name']);
$this->assertSame(5, $result['summary']['total_items']);
$this->assertSame(3, $result['summary']['missing_items']);
$this->assertSame(2, $result['summary']['submitted_items']);
$this->assertSame(40, $result['summary']['submission_percentage']);
}
public function test_report_returns_no_students_status_when_section_has_no_students(): void
{
DB::table('teacher_class')->insert([
[
'class_section_id' => 11,
'teacher_id' => 200,
'position' => 'main',
'school_year' => '2025-2026',
'semester' => 'Fall',
],
]);
DB::table('classSection')->insert([
[
'class_section_id' => 11,
'class_section_name' => 'Grade 6A',
],
]);
DB::table('users')->insert([
[
'id' => 200,
'firstname' => 'Solo',
'lastname' => 'Teacher',
'email' => 'solo@test.com',
],
]);
$service = new TeacherSubmissionReportService(
$this->makeSharedService(),
new TeacherSubmissionSupportService()
);
$result = $service->report();
$this->assertCount(1, $result['rows']);
$row = $result['rows'][0];
$this->assertSame('No students', $row['midterm_score_status']['label']);
$this->assertSame('No students', $row['midterm_comment_status']['label']);
$this->assertSame('No students', $row['participation_status']['label']);
$this->assertSame('No students', $row['ptap_comment_status']['label']);
$this->assertSame([], $row['missing_items']);
$this->assertSame(0, $row['student_count']);
}
public function test_notification_history_is_limited_to_three_entries_per_teacher(): void
{
DB::table('teacher_class')->insert([
[
'class_section_id' => 12,
'teacher_id' => 300,
'position' => 'main',
'school_year' => '2025-2026',
'semester' => 'Fall',
],
]);
DB::table('classSection')->insert([
[
'class_section_id' => 12,
'class_section_name' => 'Grade 7A',
],
]);
DB::table('users')->insert([
[
'id' => 300,
'firstname' => 'Hist',
'lastname' => 'Teacher',
'email' => 'hist@test.com',
],
[
'id' => 901,
'firstname' => 'Admin',
'lastname' => 'Two',
'email' => 'admin2@test.com',
],
]);
for ($i = 0; $i < 4; $i++) {
TeacherSubmissionNotificationHistory::create([
'teacher_id' => 300,
'class_section_id' => 12,
'admin_id' => 901,
'notification_category' => 'teacher_submissions',
'message' => 'Reminder ' . $i,
'status' => 'sent',
'school_year' => '2025-2026',
'semester' => 'Fall',
'sent_at' => now()->subMinutes($i),
]);
}
$service = new TeacherSubmissionReportService(
$this->makeSharedService(),
new TeacherSubmissionSupportService()
);
$result = $service->report();
$this->assertCount(3, $result['notificationHistory'][12][300]);
}
}
}
@@ -3,125 +3,30 @@
namespace Tests\Unit\Services\Administrator;
use App\Services\Administrator\TeacherSubmissionSupportService;
use PHPUnit\Framework\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class TeacherSubmissionSupportServiceTest extends TestCase
{
protected TeacherSubmissionSupportService $service;
use RefreshDatabase;
protected function setUp(): void
public function test_submission_status_labels(): void
{
parent::setUp();
$this->service = new TeacherSubmissionSupportService();
$service = new TeacherSubmissionSupportService();
$status = $service->submissionStatus(3, 5);
$this->assertSame('Missing', $status['label']);
$this->assertFalse($status['completed']);
}
public function test_submission_status_returns_no_students_when_expected_is_zero(): void
public function test_build_missing_items(): void
{
$result = $this->service->submissionStatus(0, 0);
$this->assertSame('No students', $result['label']);
$this->assertSame('bg-secondary', $result['badge']);
$this->assertTrue($result['completed']);
}
public function test_submission_status_returns_submitted_when_filled_matches_expected(): void
{
$result = $this->service->submissionStatus(10, 10);
$this->assertSame('Submitted', $result['label']);
$this->assertSame('bg-success', $result['badge']);
$this->assertSame('10/10', $result['detail']);
$this->assertTrue($result['completed']);
}
public function test_submission_status_returns_missing_when_not_complete(): void
{
$result = $this->service->submissionStatus(7, 10);
$this->assertSame('Missing', $result['label']);
$this->assertSame('bg-danger', $result['badge']);
$this->assertSame('7/10', $result['detail']);
$this->assertFalse($result['completed']);
}
public function test_attendance_status_returns_submitted(): void
{
$result = $this->service->attendanceStatus(true);
$this->assertSame('Submitted', $result['label']);
$this->assertSame('bg-success', $result['badge']);
$this->assertTrue($result['completed']);
}
public function test_attendance_status_returns_missing(): void
{
$result = $this->service->attendanceStatus(false);
$this->assertSame('Missing', $result['label']);
$this->assertSame('bg-danger', $result['badge']);
$this->assertFalse($result['completed']);
}
public function test_build_missing_items_returns_only_incomplete_items(): void
{
$result = $this->service->buildMissingItems([
'midterm_score_status' => ['completed' => false],
'midterm_comment_status' => ['completed' => true],
'participation_status' => ['completed' => false],
'ptap_comment_status' => ['completed' => true],
$service = new TeacherSubmissionSupportService();
$items = $service->buildMissingItems([
'attendance_status' => ['completed' => false],
'midterm_score_status' => ['completed' => true],
]);
$this->assertSame([
'midterm scores',
'participation',
'attendance',
], $result);
$this->assertSame(['attendance'], $items);
}
public function test_teacher_role_priority_orders_main_before_ta_before_other(): void
{
$this->assertSame(1, $this->service->teacherRolePriority('main'));
$this->assertSame(2, $this->service->teacherRolePriority('ta'));
$this->assertSame(3, $this->service->teacherRolePriority('teacher'));
}
public function test_truncate_notification_message_strips_tags_and_truncates(): void
{
$html = '<p>Hello <strong>world</strong></p>';
$result = $this->service->truncateNotificationMessage($html, 5);
$this->assertSame('Hello…', $result);
}
public function test_parse_missing_items_payload_returns_unique_trimmed_values(): void
{
$payload = base64_encode(json_encode([
' midterm scores ',
'attendance',
'attendance',
'',
]));
$result = $this->service->parseMissingItemsPayload($payload);
$this->assertSame([
'midterm scores',
'attendance',
], $result);
}
public function test_format_missing_items_text_formats_lists_correctly(): void
{
$this->assertSame('', $this->service->formatMissingItemsText([]));
$this->assertSame('attendance', $this->service->formatMissingItemsText(['attendance']));
$this->assertSame(
'attendance and midterm scores',
$this->service->formatMissingItemsText(['attendance', 'midterm scores'])
);
$this->assertSame(
'attendance, midterm scores and participation',
$this->service->formatMissingItemsText(['attendance', 'midterm scores', 'participation'])
);
}
}
}
@@ -0,0 +1,27 @@
<?php
namespace Tests\Unit\Services\Assignment;
use App\Models\Configuration;
use App\Services\Assignment\AssignmentContextService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AssignmentContextServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_current_term_values(): void
{
DB::table('configuration')->insert([
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
]);
$service = new AssignmentContextService(new Configuration());
$this->assertSame('Fall', $service->getCurrentSemester());
$this->assertSame('2025-2026', $service->getCurrentSchoolYear());
}
}
@@ -0,0 +1,118 @@
<?php
namespace Tests\Unit\Services\Assignment;
use App\Models\StudentClass;
use App\Models\TeacherClass;
use App\Services\Assignment\AssignmentDataLoaderService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AssignmentDataLoaderServiceTest extends TestCase
{
use RefreshDatabase;
public function test_load_teacher_classes_includes_teacher_name(): void
{
$this->seedTeacherData();
$service = new AssignmentDataLoaderService(new TeacherClass(), new StudentClass());
$rows = $service->loadTeacherClasses('2025-2026');
$this->assertCount(1, $rows);
$this->assertSame('Teacher One', $rows->first()->teacher_full_name);
}
public function test_load_student_classes_filters_inactive(): void
{
$this->seedStudentData();
$service = new AssignmentDataLoaderService(new TeacherClass(), new StudentClass());
$rows = $service->loadStudentClasses('2025-2026');
$this->assertCount(1, $rows);
$this->assertSame(1, $rows->first()->student_id);
}
private function seedTeacherData(): void
{
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Teacher',
'lastname' => 'One',
'cellphone' => '5555555555',
'email' => 'teacher@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('teacher_class')->insert([
'teacher_id' => 1,
'class_section_id' => 101,
'position' => 'main',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
}
private function seedStudentData(): void
{
DB::table('students')->insert([
[
'id' => 1,
'school_id' => 'S-1',
'firstname' => 'Student',
'lastname' => 'Active',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
'semester' => 'Fall',
'is_active' => 1,
],
[
'id' => 2,
'school_id' => 'S-2',
'firstname' => 'Student',
'lastname' => 'Inactive',
'age' => 9,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
'semester' => 'Fall',
'is_active' => 0,
],
]);
DB::table('student_class')->insert([
[
'student_id' => 1,
'class_section_id' => 101,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
[
'student_id' => 2,
'class_section_id' => 101,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
]);
}
}
@@ -0,0 +1,43 @@
<?php
namespace Tests\Unit\Services\Assignment;
use App\Services\Assignment\AssignmentMetaService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AssignmentMetaServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_school_years_uses_fallback(): void
{
$service = new AssignmentMetaService();
$years = $service->getSchoolYears('2025-2026');
$this->assertSame(['2025-2026'], $years);
}
public function test_get_school_years_returns_distinct_sorted(): void
{
DB::table('teacher_class')->insert([
['class_section_id' => 101, 'teacher_id' => 1, 'position' => 'main', 'school_year' => '2024-2025'],
['class_section_id' => 102, 'teacher_id' => 2, 'position' => 'main', 'school_year' => '2025-2026'],
['class_section_id' => 103, 'teacher_id' => 3, 'position' => 'main', 'school_year' => '2025-2026'],
]);
$service = new AssignmentMetaService();
$years = $service->getSchoolYears();
$this->assertSame(['2025-2026', '2024-2025'], $years);
}
public function test_first_non_empty_returns_first_value(): void
{
$service = new AssignmentMetaService();
$value = $service->firstNonEmpty([null, '', 'Fall', 'Spring']);
$this->assertSame('Fall', $value);
}
}
@@ -0,0 +1,31 @@
<?php
namespace Tests\Unit\Services\Assignment;
use App\Models\ClassSection;
use App\Services\Assignment\AssignmentSectionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AssignmentSectionServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_section_names_map(): void
{
DB::table('classSection')->insert([
'id' => 1,
'class_id' => 1,
'class_section_id' => 101,
'class_section_name' => '1-A',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$service = new AssignmentSectionService(new ClassSection());
$map = $service->getSectionNamesMap([1]);
$this->assertSame('1-A', $map[1]);
}
}
@@ -3,55 +3,40 @@
namespace Tests\Unit\Services\Attendance;
use App\Services\Attendance\AttendancePolicyService;
use PHPUnit\Framework\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AttendancePolicyServiceTest extends TestCase
{
public function test_teacher_can_edit_draft_day(): void
use RefreshDatabase;
public function test_teacher_can_edit_draft(): void
{
$service = new AttendancePolicyService();
$allowed = $service->canEditDay(
['status' => 'draft'],
['roles' => ['Teacher'], 'permissions' => []]
);
$day = ['status' => 'draft'];
$user = [
'roles' => ['teacher'],
'permissions' => [],
];
$this->assertTrue($service->canEditDay($day, $user));
$this->assertTrue($allowed);
}
public function test_teacher_cannot_edit_finalized_day(): void
public function test_teacher_cannot_edit_finalized(): void
{
$service = new AttendancePolicyService();
$allowed = $service->canEditDay(
['status' => 'finalized'],
['roles' => ['Teacher'], 'permissions' => []]
);
$day = ['status' => 'finalized'];
$user = [
'roles' => ['teacher'],
'permissions' => [],
];
$this->assertFalse($service->canEditDay($day, $user));
$this->assertFalse($allowed);
}
public function test_admin_can_edit_finalized_day(): void
public function test_admin_like_by_permission(): void
{
$service = new AttendancePolicyService();
$isAdmin = $service->isAdminLike([], ['attendance.manage.daily']);
$day = ['status' => 'finalized'];
$user = [
'roles' => ['admin'],
'permissions' => [],
];
$this->assertTrue($service->canEditDay($day, $user));
$this->assertTrue($isAdmin);
}
public function test_is_admin_like_excludes_teacher_roles(): void
{
$service = new AttendancePolicyService();
$this->assertFalse($service->isAdminLike(['teacher']));
$this->assertFalse($service->isAdminLike(['teacher_assistant']));
$this->assertTrue($service->isAdminLike(['principal']));
}
}
}
@@ -0,0 +1,111 @@
<?php
namespace Tests\Unit\Services\Attendance;
use App\Services\Attendance\AttendanceQueryService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AttendanceQueryServiceTest extends TestCase
{
use RefreshDatabase;
public function test_teacher_grid_returns_section_labels_and_grid(): void
{
DB::table('classSection')->insert([
'id' => 1,
'class_id' => 1,
'class_section_id' => 10,
'class_section_name' => '1A',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Teacher',
'lastname' => 'One',
'cellphone' => '5555555555',
'email' => 'teacher@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('teacher_class')->insert([
'class_section_id' => 10,
'teacher_id' => 1,
'position' => 'main',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('staff_attendance')->insert([
'user_id' => 1,
'role_name' => 'teacher',
'date' => '2025-01-05',
'semester' => 'Fall',
'school_year' => '2025-2026',
'status' => 'present',
'reason' => null,
]);
$service = new AttendanceQueryService(
new \App\Models\Configuration(),
new \App\Models\AttendanceData(),
new \App\Models\AttendanceDay(),
new \App\Models\AttendanceRecord(),
new \App\Models\Student(),
new \App\Models\StudentClass(),
new \App\Models\ClassSection(),
new \App\Models\TeacherClass(),
new \App\Models\Calendar(),
new \App\Models\User(),
new \App\Models\UserRole(),
new \App\Services\Attendance\AttendanceService(
new \App\Models\Configuration(),
new \App\Models\AttendanceData(),
new \App\Models\AttendanceDay(),
new \App\Models\ClassSection(),
new \App\Models\UserRole(),
new \App\Services\Attendance\AttendancePolicyService(),
new \App\Services\Attendance\StudentAttendanceWriterService(
new \App\Models\AttendanceData(),
new \App\Models\Student(),
new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord())
),
new \App\Services\Attendance\TeacherAttendanceSubmissionService(
new \App\Models\AttendanceDay(),
new \App\Models\AttendanceData(),
new \App\Models\TeacherClass(),
new \App\Models\Student(),
new \App\Services\Attendance\AttendancePolicyService(),
new \App\Services\Attendance\StudentAttendanceWriterService(
new \App\Models\AttendanceData(),
new \App\Models\Student(),
new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord())
)
),
new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord())
),
new \App\Services\Attendance\SemesterRangeService()
);
$grid = $service->teacherGrid('Fall', '2025-2026', '2025-01-05', 10);
$this->assertSame('1A', $grid['sections'][10]);
$this->assertSame(1, count($grid['grid']));
$this->assertSame('present', $grid['grid'][0]['status']);
}
}
@@ -2,266 +2,46 @@
namespace Tests\Unit\Services\Attendance;
use App\Models\AttendanceRecord;
use App\Services\Attendance\AttendanceRecordSyncService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AttendanceRecordSyncServiceTest extends TestCase
{
use RefreshDatabase;
protected AttendanceRecordSyncService $service;
protected function setUp(): void
public function test_update_attendance_record_adjusts_totals(): void
{
parent::setUp();
$this->service = app(AttendanceRecordSyncService::class);
}
public function test_update_attendance_record_increments_and_decrements_counters(): void
{
$record = AttendanceRecord::query()->create([
'class_section_id' => 10,
'student_id' => 200,
'school_id' => 'SCH-200',
'semester' => 'Fall',
'school_year' => '2025-2026',
DB::table('attendance_record')->insert([
'class_section_id' => 1,
'student_id' => 10,
'school_id' => 'S1',
'total_presence' => 5,
'total_absence' => 2,
'total_late' => 1,
'total_absence' => 1,
'total_late' => 2,
'total_attendance' => 8,
'modified_by' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
$this->service->updateAttendanceRecord(
studentId: 200,
schoolId: 'SCH-200',
newStatus: 'absent',
semester: 'Fall',
schoolYear: '2025-2026',
oldStatus: 'present',
modifiedBy: 99
);
$record->refresh();
$this->assertSame(4, (int)$record->total_presence);
$this->assertSame(3, (int)$record->total_absence);
$this->assertSame(1, (int)$record->total_late);
$this->assertSame(99, (int)$record->modified_by);
}
public function test_update_attendance_record_does_nothing_when_record_missing(): void
{
$this->service->updateAttendanceRecord(
studentId: 999,
schoolId: 'SCH-999',
newStatus: 'late',
semester: 'Fall',
schoolYear: '2025-2026',
oldStatus: 'present',
modifiedBy: 10
);
$this->assertDatabaseMissing('attendance_record', [
'student_id' => 999,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$service = new AttendanceRecordSyncService(new \App\Models\AttendanceRecord());
$service->updateAttendanceRecord(10, 'S1', 'absent', 'Fall', '2025-2026', 'present', 1);
$row = DB::table('attendance_record')->where('student_id', 10)->first();
$this->assertSame(4, (int) $row->total_presence);
$this->assertSame(2, (int) $row->total_absence);
}
public function test_update_attendance_record_never_goes_below_zero(): void
public function test_add_new_attendance_record_creates_row(): void
{
$record = AttendanceRecord::query()->create([
'class_section_id' => 11,
'student_id' => 201,
'school_id' => 'SCH-201',
'semester' => 'Fall',
'school_year' => '2025-2026',
'total_presence' => 0,
'total_absence' => 0,
'total_late' => 0,
'total_attendance' => 0,
'modified_by' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
$this->service->updateAttendanceRecord(
studentId: 201,
schoolId: 'SCH-201',
newStatus: 'present',
semester: 'Fall',
schoolYear: '2025-2026',
oldStatus: 'absent',
modifiedBy: 5
);
$record->refresh();
$this->assertSame(1, (int)$record->total_presence);
$this->assertSame(0, (int)$record->total_absence);
$this->assertSame(0, (int)$record->total_late);
}
public function test_add_new_attendance_record_creates_record_when_missing(): void
{
$this->service->addNewAttendanceRecord(
classSectionId: 20,
studentId: 300,
schoolId: 'SCH-300',
status: 'late',
semester: 'Spring',
schoolYear: '2025-2026',
modifiedBy: 88
);
$service = new AttendanceRecordSyncService(new \App\Models\AttendanceRecord());
$service->addNewAttendanceRecord(1, 10, 'S1', 'present', 'Fall', '2025-2026', 1);
$this->assertDatabaseHas('attendance_record', [
'class_section_id' => 20,
'student_id' => 300,
'school_id' => 'SCH-300',
'semester' => 'Spring',
'school_year' => '2025-2026',
'total_presence' => 0,
'total_absence' => 0,
'total_late' => 1,
'total_attendance' => 1,
'modified_by' => 88,
]);
}
public function test_add_new_attendance_record_updates_existing_record(): void
{
$record = AttendanceRecord::query()->create([
'class_section_id' => 21,
'student_id' => 301,
'school_id' => 'SCH-301',
'semester' => 'Spring',
'school_year' => '2025-2026',
'student_id' => 10,
'total_presence' => 1,
'total_absence' => 2,
'total_late' => 3,
'total_attendance' => 6,
'modified_by' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
$this->service->addNewAttendanceRecord(
classSectionId: 21,
studentId: 301,
schoolId: 'SCH-301',
status: 'present',
semester: 'Spring',
schoolYear: '2025-2026',
modifiedBy: 77
);
$record->refresh();
$this->assertSame(2, (int)$record->total_presence);
$this->assertSame(2, (int)$record->total_absence);
$this->assertSame(3, (int)$record->total_late);
$this->assertSame(7, (int)$record->total_attendance);
$this->assertSame(77, (int)$record->modified_by);
}
public function test_upsert_attendance_record_totals_updates_existing_record(): void
{
$record = AttendanceRecord::query()->create([
'class_section_id' => 30,
'student_id' => 400,
'school_id' => 'SCH-400',
'semester' => 'Fall',
'school_year' => '2025-2026',
'total_presence' => 0,
'total_absence' => 0,
'total_late' => 0,
'total_attendance' => 0,
'modified_by' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
$result = $this->service->upsertAttendanceRecordTotals(
studentId: 400,
classSectionId: '30',
schoolId: 'SCH-400',
semester: 'Fall',
schoolYear: '2025-2026',
totalPresence: 7,
totalLate: 2,
totalAbsence: 1,
totalAttendance: 10,
modifiedBy: 44
);
$this->assertTrue($result);
$record->refresh();
$this->assertSame(7, (int)$record->total_presence);
$this->assertSame(2, (int)$record->total_late);
$this->assertSame(1, (int)$record->total_absence);
$this->assertSame(10, (int)$record->total_attendance);
$this->assertSame(44, (int)$record->modified_by);
}
public function test_upsert_attendance_record_totals_creates_new_record(): void
{
$result = $this->service->upsertAttendanceRecordTotals(
studentId: 401,
classSectionId: '31',
schoolId: 'SCH-401',
semester: 'Fall',
schoolYear: '2025-2026',
totalPresence: 3,
totalLate: 1,
totalAbsence: 2,
totalAttendance: 6,
modifiedBy: 55
);
$this->assertTrue($result);
$this->assertDatabaseHas('attendance_record', [
'student_id' => 401,
'class_section_id' => 31,
'school_id' => 'SCH-401',
'semester' => 'Fall',
'school_year' => '2025-2026',
'total_presence' => 3,
'total_late' => 1,
'total_absence' => 2,
'total_attendance' => 6,
'modified_by' => 55,
'total_attendance' => 1,
]);
}
public function test_upsert_attendance_record_totals_returns_false_when_semester_or_school_year_missing(): void
{
$result = $this->service->upsertAttendanceRecordTotals(
studentId: 402,
classSectionId: '32',
schoolId: 'SCH-402',
semester: '',
schoolYear: '',
totalPresence: 1,
totalLate: 1,
totalAbsence: 1,
totalAttendance: 3,
modifiedBy: 1
);
$this->assertFalse($result);
$this->assertDatabaseMissing('attendance_record', [
'student_id' => 402,
'school_id' => 'SCH-402',
]);
}
}
}
@@ -0,0 +1,29 @@
<?php
namespace Tests\Unit\Services\Attendance;
use App\Services\Attendance\SemesterRangeService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AttendanceSemesterRangeServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_semester_range(): void
{
$service = new SemesterRangeService();
$range = $service->getSemesterRange('2025-2026', 'fall');
$this->assertSame(['2025-09-21', '2026-01-18'], $range);
}
public function test_build_sunday_list_returns_sundays(): void
{
$service = new SemesterRangeService();
$dates = $service->buildSundayList('2025-09-01', '2025-09-30');
$this->assertContains('2025-09-07', $dates);
$this->assertContains('2025-09-14', $dates);
}
}
@@ -0,0 +1,128 @@
<?php
namespace Tests\Unit\Services\Attendance;
use App\Models\AttendanceData;
use App\Models\AttendanceDay;
use App\Models\ClassSection;
use App\Models\Configuration;
use App\Models\User;
use App\Models\UserRole;
use App\Services\Attendance\AttendancePolicyService;
use App\Services\Attendance\AttendanceRecordSyncService;
use App\Services\Attendance\AttendanceService;
use App\Services\Attendance\StudentAttendanceWriterService;
use App\Services\Attendance\TeacherAttendanceSubmissionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class AttendanceServiceTest extends TestCase
{
use RefreshDatabase;
public function test_update_attendance_management_throws_when_locked(): void
{
$user = $this->seedUser();
$policy = Mockery::mock(AttendancePolicyService::class);
$policy->shouldReceive('canEditDay')->andReturn(false);
$writer = Mockery::mock(StudentAttendanceWriterService::class);
$teacherSubmission = Mockery::mock(TeacherAttendanceSubmissionService::class);
$recordSync = Mockery::mock(AttendanceRecordSyncService::class);
$service = new AttendanceService(
new Configuration(),
new AttendanceData(),
new AttendanceDay(),
new ClassSection(),
new UserRole(),
$policy,
$writer,
$teacherSubmission,
$recordSync
);
$this->expectException(\RuntimeException::class);
$service->updateAttendanceManagement($user, [
'class_section_id' => 1,
'student_id' => 1,
'status' => 'present',
'date' => '2025-01-01',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
}
public function test_update_attendance_management_saves_row(): void
{
$user = $this->seedUser();
$policy = Mockery::mock(AttendancePolicyService::class);
$policy->shouldReceive('canEditDay')->andReturn(true);
$writer = Mockery::mock(StudentAttendanceWriterService::class);
$writer->shouldReceive('saveOrUpdateStudentAttendance')->once();
$teacherSubmission = Mockery::mock(TeacherAttendanceSubmissionService::class);
$recordSync = Mockery::mock(AttendanceRecordSyncService::class);
$service = new AttendanceService(
new Configuration(),
new AttendanceData(),
new AttendanceDay(),
new ClassSection(),
new UserRole(),
$policy,
$writer,
$teacherSubmission,
$recordSync
);
$result = $service->updateAttendanceManagement($user, [
'class_section_id' => 1,
'student_id' => 1,
'status' => 'present',
'date' => '2025-01-01',
'semester' => 'Fall',
'school_year' => '2025-2026',
'class_id' => 1,
]);
$this->assertTrue($result['ok']);
$this->assertDatabaseHas('attendance_day', [
'class_section_id' => 1,
'date' => '2025-01-01',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
}
private function seedUser(): User
{
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Test',
'lastname' => 'User',
'cellphone' => '5555555555',
'email' => 'test@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',
]);
return User::query()->findOrFail(1);
}
}
@@ -0,0 +1,90 @@
<?php
namespace Tests\Unit\Services\Attendance;
use App\Services\Attendance\StaffAttendanceService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class StaffAttendanceServiceTest extends TestCase
{
use RefreshDatabase;
public function test_month_data_returns_sections_and_days(): void
{
DB::table('configuration')->insert([
['config_key' => 'semester', 'config_value' => 'Fall'],
['config_key' => 'school_year', 'config_value' => '2025-2026'],
]);
DB::table('classSection')->insert([
'id' => 1,
'class_id' => 1,
'class_section_id' => 10,
'class_section_name' => '1A',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Teacher',
'lastname' => 'One',
'cellphone' => '5555555555',
'email' => 'teacher@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('teacher_class')->insert([
'class_section_id' => 10,
'teacher_id' => 1,
'position' => 'main',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('staff_attendance')->insert([
'user_id' => 1,
'role_name' => 'teacher',
'date' => '2025-09-07',
'semester' => 'Fall',
'school_year' => '2025-2026',
'status' => 'present',
'reason' => null,
]);
DB::table('calendar_events')->insert([
'title' => 'No School',
'date' => '2025-09-14',
'no_school' => 1,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$service = new StaffAttendanceService(
new \App\Models\Configuration(),
new \App\Models\StaffAttendance(),
new \App\Models\TeacherClass(),
new \App\Models\ClassSection(),
new \App\Services\Attendance\SemesterRangeService()
);
$payload = $service->monthData('Fall', '2025-2026');
$this->assertNotEmpty($payload['sections']);
$this->assertContains('2025-09-14', $payload['noSchoolDays']);
}
}
@@ -2,321 +2,90 @@
namespace Tests\Unit\Services\Attendance;
use App\Models\AttendanceData;
use App\Models\AttendanceRecord;
use App\Models\Student;
use App\Services\Attendance\AttendanceRecordSyncService;
use App\Services\Attendance\StudentAttendanceWriterService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use RuntimeException;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class StudentAttendanceWriterServiceTest extends TestCase
{
use RefreshDatabase;
protected StudentAttendanceWriterService $service;
protected function setUp(): void
public function test_resolve_student_school_id_from_student(): void
{
parent::setUp();
$this->service = app(StudentAttendanceWriterService::class);
}
public function test_resolve_student_school_id_returns_passed_value_when_present(): void
{
$result = $this->service->resolveStudentSchoolId(100, 'SCH-100');
$this->assertSame('SCH-100', $result);
}
public function test_resolve_student_school_id_fetches_from_database_when_missing(): void
{
Student::query()->create([
'id' => 101,
'firstname' => 'John',
'lastname' => 'Doe',
'school_id' => 'SCH-101',
'is_active' => 1,
'created_at' => now(),
'updated_at' => now(),
DB::table('students')->insert([
'id' => 1,
'school_id' => 'S1',
'firstname' => 'Test',
'lastname' => 'Student',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
$result = $this->service->resolveStudentSchoolId(101, null);
$sync = Mockery::mock(AttendanceRecordSyncService::class);
$service = new StudentAttendanceWriterService(new \App\Models\AttendanceData(), new \App\Models\Student(), $sync);
$this->assertSame('SCH-101', $result);
$this->assertSame('S1', $service->resolveStudentSchoolId(1, null));
}
public function test_resolve_student_school_id_throws_when_student_missing(): void
public function test_save_or_update_updates_existing_and_syncs_record(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Student school ID not found.');
DB::table('students')->insert([
'id' => 1,
'school_id' => 'S1',
'firstname' => 'Test',
'lastname' => 'Student',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
$this->service->resolveStudentSchoolId(9999, null);
}
DB::table('attendance_data')->insert([
'class_id' => 1,
'class_section_id' => 1,
'student_id' => 1,
'school_id' => 'S1',
'date' => '2025-01-01',
'status' => 'present',
'is_reported' => 'no',
'is_notified' => 'no',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
public function test_insert_attendance_data_creates_new_row(): void
{
$this->service->insertAttendanceData(
classSectionId: 10,
studentId: 200,
studentSchoolId: 'SCH-200',
status: 'Absent',
reason: 'Sick',
reported: 'yes',
semester: 'Fall',
schoolYear: '2025-2026',
date: '2025-10-05',
classId: 5,
userId: 22
);
$sync = Mockery::mock(AttendanceRecordSyncService::class);
$sync->shouldReceive('updateAttendanceRecord')->once();
$this->assertDatabaseHas('attendance_data', [
'class_id' => 5,
'class_section_id' => 10,
'student_id' => 200,
'school_id' => 'SCH-200',
$service = new StudentAttendanceWriterService(new \App\Models\AttendanceData(), new \App\Models\Student(), $sync);
$service->saveOrUpdateStudentAttendance([
'class_section_id' => 1,
'student_id' => 1,
'school_id' => null,
'status' => 'absent',
'reason' => 'Sick',
'is_reported' => 'yes',
'is_notified' => 'no',
'semester' => 'Fall',
'school_year' => '2025-2026',
'date' => '2025-10-05',
'modified_by' => 22,
'date' => '2025-01-01',
'class_id' => 1,
'user_id' => 1,
]);
}
public function test_update_attendance_data_updates_existing_row(): void
{
$row = AttendanceData::query()->create([
'class_id' => 6,
'class_section_id' => 12,
'student_id' => 201,
'school_id' => 'SCH-201',
'status' => 'present',
'reason' => null,
'is_reported' => 'no',
'is_notified' => 'no',
'semester' => 'Fall',
'school_year' => '2025-2026',
'date' => '2025-10-12',
'modified_by' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
$this->service->updateAttendanceData(
attendanceId: $row->id,
newStatus: 'late',
newReason: 'Traffic',
reported: 'yes',
userId: 99
);
$row->refresh();
$this->assertSame('late', $row->status);
$this->assertSame('Traffic', $row->reason);
$this->assertSame('yes', $row->is_reported);
$this->assertSame(99, (int)$row->modified_by);
}
public function test_save_or_update_student_attendance_creates_new_attendance_and_record(): void
{
Student::query()->create([
'id' => 300,
'firstname' => 'Sara',
'lastname' => 'Smith',
'school_id' => 'SCH-300',
'is_active' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
$result = $this->service->saveOrUpdateStudentAttendance([
'class_section_id' => 15,
'student_id' => 300,
'school_id' => null,
'status' => 'present',
'reason' => 'On time',
'is_reported' => 'no',
'semester' => 'Fall',
'school_year' => '2025-2026',
'date' => '2025-10-19',
'class_id' => 9,
'user_id' => 88,
]);
$this->assertSame('created', $result['mode']);
$this->assertSame('present', $result['new_status']);
$this->assertSame('SCH-300', $result['school_id']);
$this->assertDatabaseHas('attendance_data', [
'student_id' => 300,
'school_id' => 'SCH-300',
'status' => 'present',
'semester' => 'Fall',
'school_year' => '2025-2026',
'date' => '2025-10-19',
]);
$this->assertDatabaseHas('attendance_record', [
'student_id' => 300,
'school_id' => 'SCH-300',
'semester' => 'Fall',
'school_year' => '2025-2026',
'total_presence' => 1,
'total_absence' => 0,
'total_late' => 0,
'total_attendance' => 1,
'modified_by' => 88,
]);
}
public function test_save_or_update_student_attendance_updates_existing_attendance_and_record(): void
{
Student::query()->create([
'id' => 301,
'firstname' => 'Ali',
'lastname' => 'Khan',
'school_id' => 'SCH-301',
'is_active' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
AttendanceRecord::query()->create([
'class_section_id' => 16,
'student_id' => 301,
'school_id' => 'SCH-301',
'semester' => 'Fall',
'school_year' => '2025-2026',
'total_presence' => 2,
'total_absence' => 0,
'total_late' => 0,
'total_attendance' => 2,
'modified_by' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
$attendance = AttendanceData::query()->create([
'class_id' => 10,
'class_section_id' => 16,
'student_id' => 301,
'school_id' => 'SCH-301',
'status' => 'present',
'reason' => null,
'is_reported' => 'no',
'is_notified' => 'no',
'semester' => 'Fall',
'school_year' => '2025-2026',
'date' => '2025-10-26',
'modified_by' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
$result = $this->service->saveOrUpdateStudentAttendance([
'class_section_id' => 16,
'student_id' => 301,
'school_id' => 'SCH-301',
'student_id' => 1,
'status' => 'absent',
'reason' => 'Family trip',
'reason' => 'Sick',
'is_reported' => 'yes',
'semester' => 'Fall',
'school_year' => '2025-2026',
'date' => '2025-10-26',
'class_id' => 10,
'user_id' => 77,
]);
$this->assertSame('updated', $result['mode']);
$this->assertSame('present', $result['old_status']);
$this->assertSame('absent', $result['new_status']);
$attendance->refresh();
$this->assertSame('absent', $attendance->status);
$this->assertSame('Family trip', $attendance->reason);
$this->assertSame('yes', $attendance->is_reported);
$this->assertSame(77, (int)$attendance->modified_by);
$this->assertDatabaseHas('attendance_record', [
'student_id' => 301,
'semester' => 'Fall',
'school_year' => '2025-2026',
'total_presence' => 1,
'total_absence' => 1,
'total_late' => 0,
]);
}
public function test_save_or_update_student_attendance_updates_existing_without_counter_change_when_status_same(): void
{
Student::query()->create([
'id' => 302,
'firstname' => 'Mona',
'lastname' => 'Lee',
'school_id' => 'SCH-302',
'is_active' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
$record = AttendanceRecord::query()->create([
'class_section_id' => 17,
'student_id' => 302,
'school_id' => 'SCH-302',
'semester' => 'Fall',
'school_year' => '2025-2026',
'total_presence' => 4,
'total_absence' => 1,
'total_late' => 0,
'total_attendance' => 5,
'modified_by' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
AttendanceData::query()->create([
'class_id' => 11,
'class_section_id' => 17,
'student_id' => 302,
'school_id' => 'SCH-302',
'status' => 'present',
'reason' => null,
'is_reported' => 'no',
'is_notified' => 'no',
'semester' => 'Fall',
'school_year' => '2025-2026',
'date' => '2025-11-02',
'modified_by' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
$this->service->saveOrUpdateStudentAttendance([
'class_section_id' => 17,
'student_id' => 302,
'school_id' => 'SCH-302',
'status' => 'present',
'reason' => 'Still present',
'is_reported' => 'no',
'semester' => 'Fall',
'school_year' => '2025-2026',
'date' => '2025-11-02',
'class_id' => 11,
'user_id' => 66,
]);
$record->refresh();
$this->assertSame(4, (int)$record->total_presence);
$this->assertSame(1, (int)$record->total_absence);
$this->assertSame(0, (int)$record->total_late);
$this->assertSame(5, (int)$record->total_attendance);
}
}
}
@@ -2,544 +2,80 @@
namespace Tests\Unit\Services\Attendance;
use App\Models\AttendanceData;
use App\Models\AttendanceDay;
use App\Models\Student;
use App\Models\TeacherClass;
use App\Models\User;
use App\Services\Attendance\AttendancePolicyService;
use App\Services\Attendance\StudentAttendanceWriterService;
use App\Services\Attendance\TeacherAttendanceSubmissionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use RuntimeException;
use Tests\TestCase;
class TeacherAttendanceSubmissionServiceTest extends TestCase
{
use RefreshDatabase;
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
protected function makeService(
?AttendanceDay $attendanceDay = null,
?AttendanceData $attendanceData = null,
mixed $teacherClass = null,
?Student $student = null,
mixed $attendancePolicyService = null,
mixed $studentAttendanceWriterService = null
): TeacherAttendanceSubmissionService {
return new TeacherAttendanceSubmissionService(
$attendanceDay ?? app(AttendanceDay::class),
$attendanceData ?? app(AttendanceData::class),
$teacherClass ?? app(TeacherClass::class),
$student ?? app(Student::class),
$attendancePolicyService ?? app(AttendancePolicyService::class),
$studentAttendanceWriterService ?? app(StudentAttendanceWriterService::class),
);
}
public function test_submit_throws_when_class_section_missing(): void
{
$service = $this->makeService();
$user = User::factory()->create();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Missing or invalid class section.');
$service->submit(
user: $user,
data: [
'class_section_id' => '',
'attendance' => [],
'teachers' => [],
],
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
currentSemester: fn () => 'Fall',
currentSchoolYear: fn () => '2025-2026'
);
}
public function test_submit_throws_when_section_not_found(): void
{
$service = $this->makeService();
$user = User::factory()->create();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Invalid class section.');
$service->submit(
user: $user,
data: [
'class_section_id' => '999999',
'attendance' => [['student_id' => 1, 'status' => 'present']],
'teachers' => [1 => ['status' => 'present']],
],
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
currentSemester: fn () => 'Fall',
currentSchoolYear: fn () => '2025-2026'
);
}
public function test_submit_throws_when_student_attendance_missing(): void
public function test_submit_requires_attendance_data(): void
{
DB::table('classSection')->insert([
'id' => 1,
'class_id' => 5,
'class_section_id' => 100,
'class_id' => 1,
'class_section_id' => 10,
'class_section_name' => '1A',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$teacherClassMock = Mockery::mock(TeacherClass::class);
$teacherClassMock->shouldReceive('assignedForSectionTerm')
->once()
->andReturn([
[
'teacher_id' => 55,
'position' => 'main',
'firstname' => 'Main',
'lastname' => 'Teacher',
],
]);
$user = $this->seedUser();
$policyMock = Mockery::mock(AttendancePolicyService::class);
$policyMock->shouldReceive('canEditDay')->once()->andReturnTrue();
$policy = Mockery::mock(\App\Services\Attendance\AttendancePolicyService::class);
$policy->shouldReceive('canEditDay')->andReturn(true);
$writerMock = Mockery::mock(StudentAttendanceWriterService::class);
$service = $this->makeService(
teacherClass: $teacherClassMock,
attendancePolicyService: $policyMock,
studentAttendanceWriterService: $writerMock
$service = new TeacherAttendanceSubmissionService(
new \App\Models\AttendanceDay(),
new \App\Models\AttendanceData(),
new \App\Models\TeacherClass(),
new \App\Models\Student(),
$policy,
new \App\Services\Attendance\StudentAttendanceWriterService(
new \App\Models\AttendanceData(),
new \App\Models\Student(),
new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord())
)
);
$user = User::factory()->create();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('No student attendance data provided.');
$this->expectException(\RuntimeException::class);
$service->submit(
user: $user,
data: [
'class_section_id' => '100',
'attendance' => [],
'teachers' => [55 => ['status' => 'present']],
],
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
currentSemester: fn () => 'Fall',
currentSchoolYear: fn () => '2025-2026'
$user,
['class_section_id' => 10],
fn () => false,
fn () => 'Fall',
fn () => '2025-2026'
);
}
public function test_submit_throws_when_teacher_attendance_missing(): void
private function seedUser(): User
{
DB::table('classSection')->insert([
'id' => 2,
'class_id' => 6,
'class_section_id' => 101,
]);
$service = $this->makeService();
$user = User::factory()->create();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('No teacher attendance data provided.');
$service->submit(
user: $user,
data: [
'class_section_id' => '101',
'attendance' => [
['student_id' => 1, 'status' => 'present'],
],
'teachers' => [],
],
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
currentSemester: fn () => 'Fall',
currentSchoolYear: fn () => '2025-2026'
);
}
public function test_submit_throws_when_policy_denies_edit(): void
{
DB::table('classSection')->insert([
'id' => 3,
'class_id' => 7,
'class_section_id' => 102,
]);
$teacherClassMock = Mockery::mock(TeacherClass::class);
$teacherClassMock->shouldReceive('assignedForSectionTerm')->never();
$policyMock = Mockery::mock(AttendancePolicyService::class);
$policyMock->shouldReceive('canEditDay')->once()->andReturnFalse();
$writerMock = Mockery::mock(StudentAttendanceWriterService::class);
$service = $this->makeService(
teacherClass: $teacherClassMock,
attendancePolicyService: $policyMock,
studentAttendanceWriterService: $writerMock
);
$user = User::factory()->create();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Attendance is locked for your role.');
$service->submit(
user: $user,
data: [
'class_section_id' => '102',
'attendance' => [
['student_id' => 1, 'status' => 'present'],
],
'teachers' => [
55 => ['status' => 'present'],
],
],
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
currentSemester: fn () => 'Fall',
currentSchoolYear: fn () => '2025-2026'
);
}
public function test_submit_throws_when_assigned_teacher_status_missing_or_invalid(): void
{
DB::table('classSection')->insert([
'id' => 4,
'class_id' => 8,
'class_section_id' => 103,
]);
$teacherClassMock = Mockery::mock(TeacherClass::class);
$teacherClassMock->shouldReceive('assignedForSectionTerm')
->once()
->andReturn([
[
'teacher_id' => 77,
'position' => 'main',
'firstname' => 'Main',
'lastname' => 'Teacher',
],
]);
$policyMock = Mockery::mock(AttendancePolicyService::class);
$policyMock->shouldReceive('canEditDay')->once()->andReturnTrue();
$writerMock = Mockery::mock(StudentAttendanceWriterService::class);
$service = $this->makeService(
teacherClass: $teacherClassMock,
attendancePolicyService: $policyMock,
studentAttendanceWriterService: $writerMock
);
$user = User::factory()->create();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Please fill teacher attendance for all assigned teachers.');
$service->submit(
user: $user,
data: [
'class_section_id' => '103',
'attendance' => [
['student_id' => 1, 'status' => 'present'],
],
'teachers' => [
77 => ['status' => ''],
],
],
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
currentSemester: fn () => 'Fall',
currentSchoolYear: fn () => '2025-2026'
);
}
public function test_submit_saves_teacher_staff_attendance_and_student_rows(): void
{
DB::table('classSection')->insert([
'id' => 5,
'class_id' => 9,
'class_section_id' => 104,
]);
$teacherClassMock = Mockery::mock(TeacherClass::class);
$teacherClassMock->shouldReceive('assignedForSectionTerm')
->once()
->andReturn([
[
'teacher_id' => 88,
'position' => 'main',
'firstname' => 'Lead',
'lastname' => 'Teacher',
],
[
'teacher_id' => 89,
'position' => 'ta',
'firstname' => 'Assist',
'lastname' => 'Teacher',
],
]);
$policyMock = Mockery::mock(AttendancePolicyService::class);
$policyMock->shouldReceive('canEditDay')->once()->andReturnTrue();
$policyMock->shouldReceive('isTeacher')->once()->andReturnTrue();
$writerMock = Mockery::mock(StudentAttendanceWriterService::class);
$writerMock->shouldReceive('saveOrUpdateStudentAttendance')
->twice()
->withArgs(function (array $payload) {
return in_array($payload['student_id'], [501, 502], true)
&& $payload['class_section_id'] === 104
&& $payload['class_id'] === 9
&& $payload['semester'] === 'Fall'
&& $payload['school_year'] === '2025-2026';
})
->andReturn(['mode' => 'created']);
$service = $this->makeService(
teacherClass: $teacherClassMock,
attendancePolicyService: $policyMock,
studentAttendanceWriterService: $writerMock
);
$user = User::factory()->create();
$result = $service->submit(
user: $user,
data: [
'class_section_id' => '104',
'date' => '2025-10-05',
'attendance' => [
[
'student_id' => 501,
'school_id' => 'SCH-501',
'status' => 'present',
'reason' => '',
],
[
'student_id' => 502,
'school_id' => 'SCH-502',
'status' => 'late',
'reason' => 'Bus delay',
],
],
'teachers' => [
88 => ['status' => 'present', 'reason' => null],
89 => ['status' => 'late', 'reason' => 'Traffic'],
],
],
detectExternalSubmission: fn (?array $row) => ['locked' => false, 'source' => null],
currentSemester: fn () => 'Fall',
currentSchoolYear: fn () => '2025-2026'
);
$this->assertSame(['ok' => true, 'message' => 'Attendance submitted successfully.'], $result);
$this->assertDatabaseHas('staff_attendance', [
'user_id' => 88,
'date' => '2025-10-05',
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Teacher',
'lastname' => 'One',
'cellphone' => '5555555555',
'email' => 'teacher@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',
'class_section_id' => 104,
'position' => 'main',
'status' => 'present',
]);
$this->assertDatabaseHas('staff_attendance', [
'user_id' => 89,
'date' => '2025-10-05',
'semester' => 'Fall',
'school_year' => '2025-2026',
'class_section_id' => 104,
'position' => 'ta',
'status' => 'late',
'reason' => 'Traffic',
]);
$this->assertDatabaseHas('attendance_day', [
'class_section_id' => 104,
'date' => '2025-10-05',
'semester' => 'Fall',
'school_year' => '2025-2026',
'status' => 'submitted',
'submitted_by' => $user->id,
]);
return User::query()->findOrFail(1);
}
public function test_submit_skips_locked_existing_student_rows_for_teacher(): void
{
DB::table('classSection')->insert([
'id' => 6,
'class_id' => 10,
'class_section_id' => 105,
]);
AttendanceData::query()->create([
'class_id' => 10,
'class_section_id' => 105,
'student_id' => 601,
'school_id' => 'SCH-601',
'status' => 'absent',
'reason' => 'Parent reported',
'is_reported' => 'yes',
'is_notified' => 'no',
'semester' => 'Fall',
'school_year' => '2025-2026',
'date' => '2025-10-12',
'modified_by' => 2,
'created_at' => now(),
'updated_at' => now(),
]);
$teacherClassMock = Mockery::mock(TeacherClass::class);
$teacherClassMock->shouldReceive('assignedForSectionTerm')
->once()
->andReturn([
[
'teacher_id' => 90,
'position' => 'main',
'firstname' => 'Lead',
'lastname' => 'Teacher',
],
]);
$policyMock = Mockery::mock(AttendancePolicyService::class);
$policyMock->shouldReceive('canEditDay')->once()->andReturnTrue();
$policyMock->shouldReceive('isTeacher')->once()->andReturnTrue();
$writerMock = Mockery::mock(StudentAttendanceWriterService::class);
$writerMock->shouldNotReceive('saveOrUpdateStudentAttendance');
$service = $this->makeService(
teacherClass: $teacherClassMock,
attendancePolicyService: $policyMock,
studentAttendanceWriterService: $writerMock
);
$user = User::factory()->create();
$result = $service->submit(
user: $user,
data: [
'class_section_id' => '105',
'date' => '2025-10-12',
'attendance' => [
[
'student_id' => 601,
'school_id' => 'SCH-601',
'status' => 'present',
'reason' => '',
],
],
'teachers' => [
90 => ['status' => 'present', 'reason' => null],
],
],
detectExternalSubmission: fn (?array $row) => ['locked' => true, 'source' => 'parent'],
currentSemester: fn () => 'Fall',
currentSchoolYear: fn () => '2025-2026'
);
$this->assertTrue($result['ok']);
$this->assertDatabaseHas('attendance_day', [
'class_section_id' => 105,
'date' => '2025-10-12',
'semester' => 'Fall',
'school_year' => '2025-2026',
'status' => 'submitted',
]);
}
public function test_submit_allows_non_teacher_context_to_write_existing_rows(): void
{
DB::table('classSection')->insert([
'id' => 7,
'class_id' => 11,
'class_section_id' => 106,
]);
AttendanceData::query()->create([
'class_id' => 11,
'class_section_id' => 106,
'student_id' => 701,
'school_id' => 'SCH-701',
'status' => 'absent',
'reason' => 'Parent reported',
'is_reported' => 'yes',
'is_notified' => 'no',
'semester' => 'Fall',
'school_year' => '2025-2026',
'date' => '2025-10-19',
'modified_by' => 2,
'created_at' => now(),
'updated_at' => now(),
]);
$teacherClassMock = Mockery::mock(TeacherClass::class);
$teacherClassMock->shouldReceive('assignedForSectionTerm')
->once()
->andReturn([
[
'teacher_id' => 91,
'position' => 'main',
'firstname' => 'Lead',
'lastname' => 'Teacher',
],
]);
$policyMock = Mockery::mock(AttendancePolicyService::class);
$policyMock->shouldReceive('canEditDay')->once()->andReturnTrue();
$policyMock->shouldReceive('isTeacher')->once()->andReturnFalse();
$writerMock = Mockery::mock(StudentAttendanceWriterService::class);
$writerMock->shouldReceive('saveOrUpdateStudentAttendance')
->once()
->withArgs(function (array $payload) {
return $payload['student_id'] === 701
&& $payload['class_section_id'] === 106
&& $payload['class_id'] === 11;
})
->andReturn(['mode' => 'updated']);
$service = $this->makeService(
teacherClass: $teacherClassMock,
attendancePolicyService: $policyMock,
studentAttendanceWriterService: $writerMock
);
$user = User::factory()->create();
$result = $service->submit(
user: $user,
data: [
'class_section_id' => '106',
'date' => '2025-10-19',
'attendance' => [
[
'student_id' => 701,
'school_id' => 'SCH-701',
'status' => 'present',
'reason' => '',
],
],
'teachers' => [
91 => ['status' => 'present', 'reason' => null],
],
],
detectExternalSubmission: fn (?array $row) => ['locked' => true, 'source' => 'parent'],
currentSemester: fn () => 'Fall',
currentSchoolYear: fn () => '2025-2026'
);
$this->assertTrue($result['ok']);
}
}
}
@@ -2,44 +2,28 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceData;
use App\Models\AttendanceTracking;
use App\Models\Student;
use App\Services\AttendanceCaseQueryService;
use App\Services\AttendanceParentLookupService;
use App\Services\ViolationRuleEngineService;
use App\Services\AttendanceTracking\AttendanceCaseQueryService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class AttendanceCaseQueryServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_returns_not_found_when_student_missing(): void
{
Mockery::close();
parent::tearDown();
}
public function test_get_student_case_returns_404_when_student_not_found(): void
{
$student = Mockery::mock(Student::class);
$student->shouldReceive('query->find')->with(99)->andReturn(null);
$attendanceData = Mockery::mock(AttendanceData::class);
$tracking = Mockery::mock(AttendanceTracking::class);
$rules = Mockery::mock(ViolationRuleEngineService::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new AttendanceCaseQueryService(
$student,
$attendanceData,
$tracking,
$rules,
$parents
new \App\Models\Student(),
new \App\Models\AttendanceData(),
new \App\Models\AttendanceTracking(),
Mockery::mock(\App\Services\AttendanceTracking\ViolationRuleEngineService::class),
Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class)
);
$result = $service->getStudentCase(99);
$result = $service->getStudentCase(999);
$this->assertFalse($result['success']);
$this->assertSame(404, $result['status']);
}
}
}
@@ -2,72 +2,62 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceData;
use App\Models\Configuration;
use App\Models\Student;
use App\Services\AttendanceCommunicationSupportService;
use App\Services\AttendanceEmailComposerService;
use App\Services\AttendanceParentLookupService;
use App\Services\AttendanceTracking\AttendanceCommunicationSupportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class AttendanceCommunicationSupportServiceTest extends TestCase
{
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
use RefreshDatabase;
protected function makeService(
?Student $student = null,
?AttendanceData $attendanceData = null,
?Configuration $config = null,
?AttendanceParentLookupService $parents = null,
?AttendanceEmailComposerService $composer = null
): AttendanceCommunicationSupportService {
$student ??= Mockery::mock(Student::class);
$attendanceData ??= Mockery::mock(AttendanceData::class);
$config ??= Mockery::mock(Configuration::class);
$parents ??= Mockery::mock(AttendanceParentLookupService::class);
$composer ??= Mockery::mock(AttendanceEmailComposerService::class);
$config->shouldReceive('getConfig')->with('semester')->andReturn('Fall');
$config->shouldReceive('getConfig')->with('school_year')->andReturn('2025-2026');
return new AttendanceCommunicationSupportService(
$student,
$attendanceData,
$config,
$parents,
$composer
);
}
public function test_compose_returns_422_when_student_id_missing(): void
public function test_compose_requires_student_id(): void
{
$service = $this->makeService();
$result = $service->compose(0);
$this->assertFalse($result['success']);
$this->assertSame(422, $result['status']);
}
public function test_parents_info_delegates_to_parent_lookup_service(): void
public function test_get_violation_dates_for_student(): void
{
$parents = Mockery::mock(AttendanceParentLookupService::class);
$parents->shouldReceive('parentsInfo')
->once()
->with(20, '2025-2026')
->andReturn(['success' => true, 'data' => ['primary' => [], 'secondary' => []]]);
DB::table('configuration')->insert([
['config_key' => 'semester', 'config_value' => 'Fall'],
['config_key' => 'school_year', 'config_value' => '2025-2026'],
]);
$service = $this->makeService(
parents: $parents
);
DB::table('attendance_data')->insert([
'class_id' => 1,
'class_section_id' => 1,
'student_id' => 1,
'school_id' => 'S1',
'date' => '2025-01-01',
'status' => 'absent',
'is_reported' => 'no',
'is_notified' => 'no',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$result = $service->parentsInfo(20);
$service = $this->makeService();
$dates = $service->getViolationDatesForStudent(1, 'ABS_1');
$this->assertTrue($result['success']);
$this->assertSame(['2025-01-01'], $dates);
}
}
private function makeService(): AttendanceCommunicationSupportService
{
$parentLookup = Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class);
$emailComposer = Mockery::mock(\App\Services\AttendanceTracking\AttendanceEmailComposerService::class);
return new AttendanceCommunicationSupportService(
new \App\Models\Student(),
new \App\Models\AttendanceData(),
new \App\Models\Configuration(),
$parentLookup,
$emailComposer
);
}
}
@@ -2,124 +2,37 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceEmailTemplate;
use App\Services\AttendanceEmailComposerService;
use Mockery;
use App\Services\AttendanceTracking\AttendanceEmailComposerService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AttendanceEmailComposerServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_render_template_replaces_tokens(): void
{
Mockery::close();
parent::tearDown();
}
public function test_build_template_context_returns_expected_placeholders(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
$context = $service->buildTemplateContext(
[
'name' => 'John Doe',
'last_date' => '2026-03-01',
],
[
'parent_name' => 'Jane Doe',
'phone' => '555-1212',
]
);
$this->assertSame('Jane Doe', $context['{{parent_name}}']);
$this->assertSame('John Doe', $context['{{student_name}}']);
$this->assertSame('2026-03-01', $context['{{incident_date}}']);
$this->assertSame('555-1212', $context['{{voicemail_phone}}']);
}
public function test_render_template_uses_model_get_template(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$model->shouldReceive('getTemplate')
->once()
->with('ABS_1', 'default')
->andReturn([
'subject' => 'Notice for {{student_name}}',
'body_html' => '<p>Date: {{incident_date}}</p>',
]);
$service = new AttendanceEmailComposerService($model);
$rendered = $service->renderTemplate('ABS_1', 'default', [
'{{student_name}}' => 'John Doe',
'{{incident_date}}' => '2026-03-01',
DB::table('email_templates')->insert([
'code' => 'ABS_1',
'variant' => 'default',
'subject' => 'Notice for {{student_name}}',
'body_html' => '<p>{{parent_name}}</p>',
'is_active' => 1,
]);
$this->assertSame(
['Notice for John Doe', '<p>Date: 2026-03-01</p>'],
$rendered
);
$service = new AttendanceEmailComposerService(new \App\Models\AttendanceEmailTemplate());
$context = ['{{student_name}}' => 'Student A', '{{parent_name}}' => 'Parent A'];
$rendered = $service->renderTemplate('ABS_1', 'default', $context);
$this->assertSame('Notice for Student A', $rendered[0]);
$this->assertSame('<p>Parent A</p>', $rendered[1]);
}
public function test_pick_variant_returns_override_when_present(): void
public function test_pick_variant_fallback(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
$this->assertSame('custom_variant', $service->pickVariant('ABS_2', 'custom_variant'));
}
public function test_pick_variant_returns_no_answer_for_abs_2(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
$service = new AttendanceEmailComposerService(new \App\Models\AttendanceEmailTemplate());
$this->assertSame('no_answer', $service->pickVariant('ABS_2'));
}
public function test_pick_variant_returns_default_for_other_codes(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
$this->assertSame('default', $service->pickVariant('ABS_1'));
}
public function test_normalize_body_to_html_keeps_html_when_tags_present(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
$body = '<p>Hello</p>';
$this->assertSame($body, $service->normalizeBodyToHtml($body));
}
public function test_normalize_body_to_html_converts_plain_text_to_br_html(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
$result = $service->normalizeBodyToHtml("Hello\nWorld");
$this->assertStringContainsString('Hello', $result);
$this->assertStringContainsString('<br', $result);
$this->assertStringContainsString('World', $result);
}
public function test_build_fallback_auto_email_returns_subject_and_body(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
[$subject, $body] = $service->buildFallbackAutoEmail('ABS_1', [
'name' => 'John Doe',
'violation' => '1 Unreported Absence',
'last_date' => '2026-03-01',
], 'Jane Doe');
$this->assertSame('Attendance Notice: Unreported Absence', $subject);
$this->assertStringContainsString('John Doe', $body);
$this->assertStringContainsString('2026-03-01', $body);
$this->assertStringContainsString('Jane Doe', $body);
}
}
}
@@ -2,45 +2,32 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\ParentNotification;
use App\Services\AttendanceNotificationLogService;
use Mockery;
use App\Services\AttendanceTracking\AttendanceNotificationLogService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AttendanceNotificationLogServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_log_notification_creates_and_updates(): void
{
Mockery::close();
parent::tearDown();
$service = new AttendanceNotificationLogService(new \App\Models\ParentNotification());
$service->logNotification(1, 'ABS_1', '2025-01-01', 'email', 'parent@example.com', 'Subject', 'sent');
$this->assertDatabaseHas('parent_notifications', [
'student_id' => 1,
'code' => 'ABS_1',
'incident_date' => '2025-01-01',
'channel' => 'email',
'to_address' => 'parent@example.com',
'status' => 'sent',
]);
$service->logNotification(1, 'ABS_1', '2025-01-01', 'email', 'parent@example.com', 'Updated', 'failed');
$row = DB::table('parent_notifications')->where('student_id', 1)->first();
$this->assertSame('Updated', $row->subject);
$this->assertSame('failed', $row->status);
}
public function test_notification_already_sent_returns_true_when_model_has_sent_returns_true(): void
{
$model = Mockery::mock(ParentNotification::class);
$model->shouldReceive('hasSent')
->once()
->with(10, 'ABS_1', '2026-03-01', 'email', 'parent@example.com')
->andReturn(true);
$service = new AttendanceNotificationLogService($model);
$this->assertTrue(
$service->notificationAlreadySent(10, 'ABS_1', '2026-03-01', 'email', 'parent@example.com')
);
}
public function test_notification_already_sent_returns_false_when_model_has_sent_returns_false(): void
{
$model = Mockery::mock(ParentNotification::class);
$model->shouldReceive('hasSent')
->once()
->andReturn(false);
$service = new AttendanceNotificationLogService($model);
$this->assertFalse(
$service->notificationAlreadySent(10, 'ABS_1', '2026-03-01', 'email', 'parent@example.com')
);
}
}
}
@@ -2,114 +2,50 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceData;
use App\Models\AttendanceTracking;
use App\Models\Configuration;
use App\Models\Student;
use App\Models\StudentClass;
use App\Services\AttendanceEmailComposerService;
use App\Services\AttendanceMailerService;
use App\Services\AttendanceNotificationLogService;
use App\Services\AttendanceNotificationWorkflowService;
use App\Services\AttendanceParentLookupService;
use App\Services\ViolationRuleEngineService;
use App\Services\AttendanceTracking\AttendanceNotificationWorkflowService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class AttendanceNotificationWorkflowServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_record_requires_parent_email(): void
{
Mockery::close();
parent::tearDown();
}
protected function makeService(
?Student $student = null,
?StudentClass $studentClass = null,
?AttendanceData $attendanceData = null,
?AttendanceTracking $tracking = null,
?Configuration $config = null,
?AttendanceMailerService $mailer = null,
?ViolationRuleEngineService $rules = null,
?AttendanceParentLookupService $parents = null,
?AttendanceEmailComposerService $composer = null,
?AttendanceNotificationLogService $log = null
): AttendanceNotificationWorkflowService {
$student ??= Mockery::mock(Student::class);
$studentClass ??= Mockery::mock(StudentClass::class);
$attendanceData ??= Mockery::mock(AttendanceData::class);
$tracking ??= Mockery::mock(AttendanceTracking::class);
$config ??= Mockery::mock(Configuration::class);
$mailer ??= Mockery::mock(AttendanceMailerService::class);
$rules ??= Mockery::mock(ViolationRuleEngineService::class);
$parents ??= Mockery::mock(AttendanceParentLookupService::class);
$composer ??= Mockery::mock(AttendanceEmailComposerService::class);
$log ??= Mockery::mock(AttendanceNotificationLogService::class);
$config->shouldReceive('getConfig')->with('semester')->andReturn('Fall');
$config->shouldReceive('getConfig')->with('school_year')->andReturn('2025-2026');
return new AttendanceNotificationWorkflowService(
$student,
$studentClass,
$attendanceData,
$tracking,
$config,
$mailer,
$rules,
$parents,
$composer,
$log
);
}
public function test_send_auto_emails_returns_empty_summary_when_no_auto_email_violations(): void
{
$service = $this->makeService();
$result = $service->sendAutoEmails([
['action' => 'team_notify'],
DB::table('students')->insert([
'id' => 1,
'school_id' => 'S1',
'firstname' => 'Student',
'lastname' => 'One',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
$this->assertTrue($result['success']);
$this->assertSame(0, $result['sent']);
$this->assertSame(0, $result['skipped']);
$this->assertSame(0, $result['errors']);
}
public function test_send_email_manual_returns_failure_when_all_sends_fail(): void
{
$mailer = Mockery::mock(AttendanceMailerService::class);
$mailer->shouldReceive('send')->andReturn(false);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$parents->shouldReceive('getSecondaryParentForStudent')->andReturn(null);
$composer = Mockery::mock(AttendanceEmailComposerService::class);
$composer->shouldReceive('normalizeBodyToHtml')->andReturn('<p>Hello</p>');
$composer->shouldReceive('renderWithEmailLayout')->andReturn('<html>Hello</html>');
$log = Mockery::mock(AttendanceNotificationLogService::class);
$log->shouldReceive('logNotification')->atLeast()->once();
$service = $this->makeService(
mailer: $mailer,
parents: $parents,
composer: $composer,
log: $log
$service = new AttendanceNotificationWorkflowService(
new \App\Models\Student(),
new \App\Models\StudentClass(),
new \App\Models\AttendanceData(),
new \App\Models\AttendanceTracking(),
new \App\Models\Configuration(),
Mockery::mock(\App\Services\AttendanceTracking\AttendanceMailerService::class),
Mockery::mock(\App\Services\AttendanceTracking\ViolationRuleEngineService::class),
Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class),
Mockery::mock(\App\Services\AttendanceTracking\AttendanceEmailComposerService::class),
Mockery::mock(\App\Services\AttendanceTracking\AttendanceNotificationLogService::class)
);
$result = $service->sendEmailManual([
'student_id' => 10,
'to' => 'parent@example.com',
'subject' => 'Test',
'body_html' => 'Hello',
'code' => 'ABS_1',
'incident_date' => '2026-03-01',
], ['2026-03-01']);
$result = $service->record([
'student_id' => 1,
'date' => '2025-01-01',
]);
$this->assertFalse($result['success']);
$this->assertSame(500, $result['status']);
$this->assertSame(422, $result['status']);
}
}
}
@@ -2,7 +2,7 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Services\AttendanceParentLookupService;
use App\Services\AttendanceTracking\AttendanceParentLookupService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
@@ -11,86 +11,78 @@ class AttendanceParentLookupServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_primary_parent_for_student_returns_parent_info(): void
public function test_get_primary_parent_for_student(): void
{
DB::table('users')->insert([
'id' => 100,
'firstname' => 'Jane',
'lastname' => 'Doe',
'email' => 'jane@example.com',
'cellphone' => '555-1111',
'id' => 1,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'One',
'cellphone' => '5555555555',
'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' => 200,
'parent_id' => 100,
'id' => 1,
'school_id' => 'S1',
'firstname' => 'Student',
'lastname' => 'One',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
$service = new AttendanceParentLookupService();
$parent = $service->getPrimaryParentForStudent(1);
$result = $service->getPrimaryParentForStudent(200);
$this->assertNotNull($result);
$this->assertSame(100, $result['user_id']);
$this->assertSame('jane@example.com', $result['email']);
$this->assertSame('Jane Doe', $result['parent_name']);
$this->assertSame('parent@example.com', $parent['email']);
}
public function test_get_secondary_parent_for_student_returns_secondary_user_when_present(): void
public function test_get_secondary_parent_falls_back_to_parent_row(): void
{
DB::table('users')->insert([
['id' => 100, 'firstname' => 'Jane', 'lastname' => 'Doe', 'email' => 'jane@example.com', 'cellphone' => '555-1111'],
['id' => 101, 'firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@example.com', 'cellphone' => '555-2222'],
]);
DB::table('students')->insert([
'id' => 200,
'parent_id' => 100,
'id' => 1,
'school_id' => 'S1',
'firstname' => 'Student',
'lastname' => 'One',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 11,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
DB::table('parents')->insert([
'firstparent_id' => 100,
'secondparent_id' => 101,
'secondparent_firstname' => 'Second',
'secondparent_lastname' => 'Parent',
'secondparent_gender' => 'Female',
'secondparent_email' => 'second@example.com',
'secondparent_phone' => '5551112222',
'firstparent_id' => 11,
'secondparent_id' => 0,
'semester' => 'Fall',
'school_year' => '2025-2026',
'updated_at' => now(),
]);
$service = new AttendanceParentLookupService();
$parent = $service->getSecondaryParentForStudent(1, '2025-2026');
$result = $service->getSecondaryParentForStudent(200, '2025-2026');
$this->assertNotNull($result);
$this->assertSame(101, $result['user_id']);
$this->assertSame('john@example.com', $result['email']);
$this->assertSame('second@example.com', $parent['email']);
}
public function test_parents_info_returns_primary_and_secondary_data(): void
{
DB::table('users')->insert([
['id' => 100, 'firstname' => 'Jane', 'lastname' => 'Doe', 'email' => 'jane@example.com', 'cellphone' => '555-1111'],
['id' => 101, 'firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@example.com', 'cellphone' => '555-2222'],
]);
DB::table('students')->insert([
'id' => 200,
'parent_id' => 100,
'school_year' => '2025-2026',
]);
DB::table('parents')->insert([
'firstparent_id' => 100,
'secondparent_id' => 101,
'school_year' => '2025-2026',
'updated_at' => now(),
]);
$service = new AttendanceParentLookupService();
$result = $service->parentsInfo(200, '2025-2026');
$this->assertTrue($result['success']);
$this->assertSame('jane@example.com', $result['data']['primary']['email']);
$this->assertSame('john@example.com', $result['data']['secondary']['email']);
}
}
}
@@ -2,49 +2,39 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceData;
use App\Services\AttendancePendingViolationService;
use App\Services\AttendanceViolationStudentResolverService;
use App\Services\ViolationRuleEngineService;
use App\Services\AttendanceTracking\AttendancePendingViolationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class AttendancePendingViolationServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_returns_error_when_no_students(): void
{
Mockery::close();
parent::tearDown();
}
$resolver = Mockery::mock(\App\Services\AttendanceTracking\AttendanceViolationStudentResolverService::class);
$resolver->shouldReceive('resolveForSchoolYear')->andReturn([
'school_year' => '2025-2026',
'semester' => 'Fall',
'student_ids' => [],
'student_codes' => [],
'students' => [],
'student_code_to_id' => [],
'existing_ids' => [],
'debug' => [],
]);
public function test_get_pending_violations_returns_error_when_no_student_identifiers_found(): void
{
$attendanceData = Mockery::mock(AttendanceData::class);
$rules = Mockery::mock(ViolationRuleEngineService::class);
$resolver = Mockery::mock(AttendanceViolationStudentResolverService::class);
$engine = Mockery::mock(\App\Services\AttendanceTracking\ViolationRuleEngineService::class);
$resolver->shouldReceive('resolveForSchoolYear')
->once()
->with('2025-2026', null)
->andReturn([
'school_year' => '2025-2026',
'semester' => null,
'student_ids' => [],
'student_codes' => [],
'students' => [],
'student_code_to_id' => [],
'existing_ids' => [],
'debug' => [
'class_students' => 0,
'student_ids' => 0,
],
]);
$service = new AttendancePendingViolationService(
new \App\Models\AttendanceData(),
$engine,
$resolver
);
$service = new AttendancePendingViolationService($attendanceData, $rules, $resolver);
$result = $service->getPendingViolations('2025-2026', null, null);
$result = $service->getPendingViolations('2025-2026');
$this->assertSame([], $result['students']);
$this->assertSame('No student identifiers found for the current school year.', $result['error']);
}
}
}
@@ -2,134 +2,17 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceData;
use App\Models\AttendanceTracking;
use App\Models\Configuration;
use App\Models\Student;
use App\Models\StudentClass;
use App\Services\AttendanceCaseQueryService;
use App\Services\AttendanceCommunicationSupportService;
use App\Services\AttendanceEmailComposerService;
use App\Services\AttendanceMailerService;
use App\Services\AttendanceNotificationLogService;
use App\Services\AttendanceNotificationWorkflowService;
use App\Services\AttendanceParentLookupService;
use App\Services\AttendancePendingViolationService;
use App\Services\AttendanceTrackingService;
use App\Services\AttendanceViolationStudentResolverService;
use App\Services\ViolationRuleEngineService;
use Mockery;
use App\Services\AttendanceTracking\AttendanceTrackingService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AttendanceTrackingServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_service_instantiates(): void
{
Mockery::close();
parent::tearDown();
$service = new AttendanceTrackingService();
$this->assertInstanceOf(AttendanceTrackingService::class, $service);
}
protected function makeService(
?AttendancePendingViolationService $pending = null,
?AttendanceCaseQueryService $caseQuery = null,
?AttendanceNotificationWorkflowService $workflow = null,
?AttendanceCommunicationSupportService $support = null
): AttendanceTrackingService {
$student = Mockery::mock(Student::class);
$studentClass = Mockery::mock(StudentClass::class);
$attendanceData = Mockery::mock(AttendanceData::class);
$tracking = Mockery::mock(AttendanceTracking::class);
$config = Mockery::mock(Configuration::class);
$mailer = Mockery::mock(AttendanceMailerService::class);
$rules = Mockery::mock(ViolationRuleEngineService::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$composer = Mockery::mock(AttendanceEmailComposerService::class);
$log = Mockery::mock(AttendanceNotificationLogService::class);
$resolver = Mockery::mock(AttendanceViolationStudentResolverService::class);
$pending ??= Mockery::mock(AttendancePendingViolationService::class);
$caseQuery ??= Mockery::mock(AttendanceCaseQueryService::class);
$workflow ??= Mockery::mock(AttendanceNotificationWorkflowService::class);
$support ??= Mockery::mock(AttendanceCommunicationSupportService::class);
$config->shouldReceive('getConfig')->with('semester')->andReturn('Fall');
$config->shouldReceive('getConfig')->with('school_year')->andReturn('2025-2026');
return new AttendanceTrackingService(
$student,
$studentClass,
$attendanceData,
$tracking,
$config,
$mailer,
$rules,
$parents,
$composer,
$log,
$resolver,
$caseQuery,
$workflow,
$support,
$pending
);
}
public function test_get_pending_violations_delegates_to_pending_service(): void
{
$pending = Mockery::mock(AttendancePendingViolationService::class);
$pending->shouldReceive('getPendingViolations')
->once()
->with('2025-2026', null, null)
->andReturn(['students' => []]);
$service = $this->makeService(pending: $pending);
$result = $service->getPendingViolations();
$this->assertSame(['students' => []], $result);
}
public function test_get_student_case_delegates_to_case_query_service(): void
{
$caseQuery = Mockery::mock(AttendanceCaseQueryService::class);
$caseQuery->shouldReceive('getStudentCase')
->once()
->andReturn(['success' => true]);
$service = $this->makeService(caseQuery: $caseQuery);
$result = $service->getStudentCase(1);
$this->assertTrue($result['success']);
}
public function test_record_delegates_to_workflow_service(): void
{
$workflow = Mockery::mock(AttendanceNotificationWorkflowService::class);
$workflow->shouldReceive('record')
->once()
->with(['student_id' => 10])
->andReturn(['success' => true]);
$service = $this->makeService(workflow: $workflow);
$result = $service->record(['student_id' => 10]);
$this->assertTrue($result['success']);
}
public function test_compose_delegates_to_support_service(): void
{
$support = Mockery::mock(AttendanceCommunicationSupportService::class);
$support->shouldReceive('compose')
->once()
->with(10, 'ABS_1', 'default', null)
->andReturn(['success' => true]);
$service = $this->makeService(support: $support);
$result = $service->compose(10);
$this->assertTrue($result['success']);
}
}
}
@@ -2,67 +2,46 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceData;
use App\Models\Student;
use App\Models\StudentClass;
use App\Services\AttendanceViolationStudentResolverService;
use Mockery;
use App\Services\AttendanceTracking\AttendanceViolationStudentResolverService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AttendanceViolationStudentResolverServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_resolve_for_school_year_returns_students(): void
{
Mockery::close();
parent::tearDown();
}
DB::table('students')->insert([
'id' => 1,
'school_id' => 'S1',
'firstname' => 'Student',
'lastname' => 'One',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
public function test_ensure_attendance_student_exists_keeps_numeric_student(): void
{
$student = Mockery::mock(Student::class);
$studentClass = Mockery::mock(StudentClass::class);
$attendanceData = Mockery::mock(AttendanceData::class);
DB::table('student_class')->insert([
'student_id' => 1,
'class_section_id' => 10,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$service = new AttendanceViolationStudentResolverService($student, $studentClass, $attendanceData);
$students = [];
$studentCodeToId = [];
$existingIds = [];
$sid = $service->ensureAttendanceStudentExists(
$students,
$studentCodeToId,
$existingIds,
123
$service = new AttendanceViolationStudentResolverService(
new \App\Models\Student(),
new \App\Models\StudentClass(),
new \App\Models\AttendanceData()
);
$this->assertSame(123, $sid);
$this->assertCount(1, $students);
$this->assertSame(123, $students[0]['id']);
$result = $service->resolveForSchoolYear('2025-2026', 'Fall');
$this->assertContains(1, $result['student_ids']);
$this->assertNotEmpty($result['students']);
}
public function test_ensure_attendance_student_exists_creates_placeholder_for_code(): void
{
$student = Mockery::mock(Student::class);
$studentClass = Mockery::mock(StudentClass::class);
$attendanceData = Mockery::mock(AttendanceData::class);
$service = new AttendanceViolationStudentResolverService($student, $studentClass, $attendanceData);
$students = [];
$studentCodeToId = [];
$existingIds = [];
$sid = $service->ensureAttendanceStudentExists(
$students,
$studentCodeToId,
$existingIds,
'SCH-001'
);
$this->assertNotNull($sid);
$this->assertCount(1, $students);
$this->assertSame('SCH-001', $students[0]['school_id']);
$this->assertArrayHasKey('SCH-001', $studentCodeToId);
}
}
}
@@ -2,111 +2,37 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceTracking;
use App\Services\AttendanceParentLookupService;
use App\Services\ViolationRuleEngineService;
use App\Services\AttendanceTracking\ViolationRuleEngineService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class ViolationRuleEngineServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_school_year_helpers(): void
{
Mockery::close();
parent::tearDown();
$service = $this->makeService();
$this->assertSame(['2025-08-01', '2026-07-31'], $service->deriveSchoolYearBounds('2025-2026'));
$this->assertSame('2025-2026', $service->schoolYearForDate('2025-09-01'));
}
public function test_choose_higher_severity_returns_higher_severity_rule(): void
public function test_window_weeks_for_violation_code(): void
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
$result = $service->chooseHigherSeverity(
['severity' => 2, 'action' => 'team_notify'],
['severity' => 4, 'action' => 'auto_email']
);
$this->assertSame(4, $result['severity']);
}
public function test_choose_higher_severity_breaks_ties_by_action_priority(): void
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
$result = $service->chooseHigherSeverity(
['severity' => 3, 'action' => 'team_notify'],
['severity' => 3, 'action' => 'auto_email']
);
$this->assertSame('team_notify', $result['action']);
}
public function test_window_weeks_for_violation_code_returns_expected_values(): void
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
$service = $this->makeService();
$this->assertSame(3, $service->windowWeeksForViolationCode('ABS_2'));
$this->assertSame(4, $service->windowWeeksForViolationCode('ABS_3'));
$this->assertSame(4, $service->windowWeeksForViolationCode('MIX'));
$this->assertSame(5, $service->windowWeeksForViolationCode('ABS_4'));
$this->assertSame(4, $service->windowWeeksForViolationCode('LATE_4'));
}
public function test_has_n_consecutive_items_detects_weekly_sequence(): void
private function makeService(): ViolationRuleEngineService
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$tracking = new \App\Models\AttendanceTracking();
$parentLookup = Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
$result = $service->hasNConsecutiveItems(
['2026-03-15', '2026-03-08', '2026-03-01'],
3,
7
);
$this->assertTrue($result);
return new ViolationRuleEngineService($tracking, $parentLookup);
}
public function test_has_n_in_w_active_weeks_detects_window_match(): void
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
$this->assertTrue($service->hasNInWActiveWeeks([0, 1], 2, 3));
$this->assertFalse($service->hasNInWActiveWeeks([0], 2, 3));
}
public function test_two_lates_one_abs_in_w_weeks_detects_mix_rule(): void
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
$this->assertTrue($service->twoLatesOneAbsInWWeeks([1, 2], [3], 4));
$this->assertFalse($service->twoLatesOneAbsInWWeeks([1], [3], 4));
}
public function test_derive_school_year_bounds_returns_expected_dates(): void
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
[$start, $end] = $service->deriveSchoolYearBounds('2025-2026');
$this->assertSame('2025-08-01', $start);
$this->assertSame('2026-07-31', $end);
}
}
}
@@ -0,0 +1,33 @@
<?php
namespace Tests\Unit\Services\Auth;
use App\Services\Auth\RegistrationCaptchaService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class RegistrationCaptchaServiceTest extends TestCase
{
use RefreshDatabase;
public function test_generate_sets_session_value(): void
{
$service = new RegistrationCaptchaService();
$value = $service->generate(6);
$this->assertNotEmpty($value);
$this->assertSame($value, session()->get('captcha_answer'));
}
public function test_verify_and_clear(): void
{
$service = new RegistrationCaptchaService();
session()->put('captcha_answer', 'ABC123');
$this->assertTrue($service->verify('ABC123'));
$this->assertFalse($service->verify('WRONG'));
$service->clear();
$this->assertNull(session()->get('captcha_answer'));
}
}
@@ -0,0 +1,70 @@
<?php
namespace Tests\Unit\Services\Auth;
use App\Services\Auth\RegistrationFormatterService;
use App\Services\PhoneFormatterService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class RegistrationFormatterServiceTest extends TestCase
{
use RefreshDatabase;
public function test_format_normalizes_parent_fields(): void
{
$formatter = new RegistrationFormatterService(new PhoneFormatterService());
$result = $formatter->format([
'firstname' => 'john',
'lastname' => 'doe',
'email' => 'USER@EXAMPLE.COM',
'gender' => 'Male',
'city' => 'new haven',
'cellphone' => '(203) 555-1234',
'address_street' => '123 main st',
'apt' => 'a1',
'state' => 'ct',
'zip' => '06510',
'accept_school_policy' => 1,
'no_second_parent_info' => 1,
]);
$this->assertSame('John', $result['firstname']);
$this->assertSame('Doe', $result['lastname']);
$this->assertSame('user@example.com', $result['email']);
$this->assertSame('New Haven', $result['city']);
$this->assertSame('203-555-1234', $result['cellphone']);
$this->assertSame('CT', $result['state']);
$this->assertArrayNotHasKey('second_firstname', $result);
}
public function test_format_includes_second_parent_when_present(): void
{
$formatter = new RegistrationFormatterService(new PhoneFormatterService());
$result = $formatter->format([
'firstname' => 'john',
'lastname' => 'doe',
'email' => 'user@example.com',
'gender' => 'Male',
'city' => 'new haven',
'cellphone' => '2035551234',
'address_street' => '123 main st',
'apt' => 'a1',
'state' => 'ct',
'zip' => '06510',
'accept_school_policy' => 1,
'second_firstname' => 'jane',
'second_lastname' => 'doe',
'second_gender' => 'Female',
'second_email' => 'jane@example.com',
'second_cellphone' => '2035559876',
]);
$this->assertSame('Jane', $result['second_firstname']);
$this->assertSame('Doe', $result['second_lastname']);
$this->assertSame('jane@example.com', $result['second_email']);
$this->assertSame('203-555-9876', $result['second_cellphone']);
}
}
@@ -0,0 +1,150 @@
<?php
namespace Tests\Unit\Services\Auth;
use App\Models\Role;
use App\Models\User;
use App\Services\Auth\RegistrationCaptchaService;
use App\Services\Auth\RegistrationFormatterService;
use App\Services\Auth\RegistrationService;
use App\Services\EmailService;
use App\Services\PhoneFormatterService;
use App\Services\SchoolIdService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class RegistrationServiceTest extends TestCase
{
use RefreshDatabase;
public function test_register_creates_parent_user(): void
{
DB::table('configuration')->insert([
['id' => 2001, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
['id' => 2002, 'config_key' => 'semester', 'config_value' => 'Fall'],
]);
Role::query()->create([
'id' => 1,
'name' => 'parent',
'slug' => 'parent',
'description' => 'Parent',
'priority' => 1,
'is_active' => 1,
]);
session()->put('captcha_answer', 'ABCD');
$emailService = Mockery::mock(EmailService::class);
$emailService->shouldReceive('send')->once()->andReturn(true);
$service = new RegistrationService(
$emailService,
new SchoolIdService(),
new RegistrationFormatterService(new PhoneFormatterService()),
new RegistrationCaptchaService()
);
$result = $service->register([
'firstname' => 'Parent',
'lastname' => 'User',
'gender' => 'Male',
'email' => 'parent@example.com',
'confirm_email' => 'parent@example.com',
'cellphone' => '5555555555',
'address_street' => '123 Main',
'apt' => 'A',
'city' => 'City',
'state' => 'CT',
'zip' => '12345',
'accept_school_policy' => 1,
'captcha' => 'ABCD',
'is_parent' => 1,
'no_second_parent_info' => 1,
]);
$this->assertTrue($result['ok']);
$this->assertDatabaseHas('users', [
'email' => 'parent@example.com',
'status' => 'Inactive',
]);
$this->assertDatabaseHas('user_roles', [
'user_id' => $result['user']->id,
'role_id' => 1,
]);
}
public function test_register_rejects_pending_activation(): void
{
DB::table('configuration')->insert([
['id' => 2001, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
['id' => 2002, 'config_key' => 'semester', 'config_value' => 'Fall'],
]);
Role::query()->create([
'id' => 1,
'name' => 'parent',
'slug' => 'parent',
'description' => 'Parent',
'priority' => 1,
'is_active' => 1,
]);
User::query()->create([
'school_id' => '2500001',
'firstname' => 'Existing',
'lastname' => 'User',
'gender' => 'Male',
'cellphone' => '555-555-5555',
'email' => 'existing@example.com',
'address_street' => '123 Main',
'city' => 'City',
'state' => 'CT',
'zip' => '12345',
'accept_school_policy' => 1,
'is_verified' => 0,
'status' => 'Inactive',
'is_suspended' => 0,
'failed_attempts' => 0,
'password' => pbkdf2_hash('secret'),
'semester' => 'Fall',
'school_year' => '2025-2026',
'token' => 'token',
]);
session()->put('captcha_answer', 'ABCD');
$emailService = Mockery::mock(EmailService::class);
$emailService->shouldNotReceive('send');
$service = new RegistrationService(
$emailService,
new SchoolIdService(),
new RegistrationFormatterService(new PhoneFormatterService()),
new RegistrationCaptchaService()
);
$result = $service->register([
'firstname' => 'Parent',
'lastname' => 'User',
'gender' => 'Male',
'email' => 'existing@example.com',
'confirm_email' => 'existing@example.com',
'cellphone' => '5555555555',
'address_street' => '123 Main',
'apt' => 'A',
'city' => 'City',
'state' => 'CT',
'zip' => '12345',
'accept_school_policy' => 1,
'captcha' => 'ABCD',
'is_parent' => 1,
'no_second_parent_info' => 1,
]);
$this->assertFalse($result['ok']);
$this->assertSame('pending_activation', $result['code']);
}
}
@@ -0,0 +1,30 @@
<?php
namespace Tests\Unit\Services\BroadcastEmail;
use App\Services\BroadcastEmail\BroadcastEmailComposerService;
use Tests\TestCase;
class BroadcastEmailComposerServiceTest extends TestCase
{
public function test_sanitize_removes_script_and_events(): void
{
$service = new BroadcastEmailComposerService();
$html = '<p onclick="alert(1)">Hi</p><script>alert(2)</script>';
$clean = $service->sanitizeHtml($html);
$this->assertStringNotContainsString('script', $clean);
$this->assertStringNotContainsString('onclick', $clean);
}
public function test_compose_replaces_name_when_personalized(): void
{
$service = new BroadcastEmailComposerService();
$html = '<p>Hello {{name}}</p>';
$rendered = $service->compose(false, 'Subject', $html, 'Parent', '', '', '', true);
$this->assertSame('<p>Hello Parent</p>', $rendered);
}
}
@@ -0,0 +1,93 @@
<?php
namespace Tests\Unit\Services\BroadcastEmail;
use App\Services\BroadcastEmail\BroadcastEmailRecipientService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class BroadcastEmailRecipientServiceTest extends TestCase
{
use RefreshDatabase;
public function test_parents_with_emails_filters_missing_email(): void
{
$this->seedParentData();
$service = new BroadcastEmailRecipientService();
$parents = $service->parentsWithEmails();
$this->assertCount(1, $parents);
$this->assertSame('parent@example.com', $parents[0]['email']);
}
public function test_recipients_by_ids_returns_names(): void
{
$this->seedParentData();
$service = new BroadcastEmailRecipientService();
$recipients = $service->recipientsByIds([10]);
$this->assertCount(1, $recipients);
$this->assertSame('Parent User', $recipients[0]['name']);
}
private function seedParentData(): 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' => '5555555555',
'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',
],
[
'id' => 11,
'school_id' => 1,
'firstname' => 'No',
'lastname' => 'Email',
'cellphone' => '5555555555',
'email' => '',
'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],
['user_id' => 11, 'role_id' => 1],
]);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Tests\Unit\Services\BroadcastEmail;
use App\Services\BroadcastEmail\BroadcastEmailSenderOptionsService;
use Tests\TestCase;
class BroadcastEmailSenderOptionsServiceTest extends TestCase
{
public function test_list_options_from_env(): void
{
$old = getenv('MAIL_SENDERS') ?: '';
putenv('MAIL_SENDERS={"general":{"name":"Office","email":"office@example.com"}}');
$service = new BroadcastEmailSenderOptionsService();
$options = $service->listOptions();
$this->assertSame('general', $options[0]['key']);
$this->assertSame('Office <office@example.com>', $options[0]['label']);
putenv('MAIL_SENDERS=' . $old);
}
}
@@ -0,0 +1,71 @@
<?php
namespace Tests\Unit\Services\ClassPrep;
use App\Services\ClassPrep\ClassRosterService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ClassRosterServiceTest extends TestCase
{
use RefreshDatabase;
public function test_list_students_by_class(): void
{
$this->seedRosterData();
$service = new ClassRosterService();
$students = $service->listStudentsByClass(101, '2025-2026');
$this->assertCount(1, $students);
$this->assertSame('1-A', $students[0]['registration_grade']);
}
private function seedRosterData(): void
{
DB::table('configuration')->insert([
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
]);
DB::table('classes')->insert([
'id' => 1,
'class_name' => 'Class 1',
'schedule' => 'Sun',
'capacity' => 20,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('classSection')->insert([
'id' => 1,
'class_id' => 1,
'class_section_id' => 101,
'class_section_name' => '1-A',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('students')->insert([
'school_id' => 'S-1',
'firstname' => 'Student',
'lastname' => 'One',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
'semester' => 'Fall',
'is_active' => 1,
'is_new' => 0,
]);
DB::table('student_class')->insert([
'student_id' => 1,
'class_section_id' => 101,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
}
}
@@ -0,0 +1,165 @@
<?php
namespace Tests\Unit\Services\ClassPrep;
use App\Services\ClassPrep\StickerCountService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class StickerCountServiceTest extends TestCase
{
use RefreshDatabase;
public function test_list_all_returns_totals_excluding_youth(): void
{
$this->seedStickerData();
$service = new StickerCountService();
$payload = $service->listAll('2025-2026', 'Fall');
$this->assertSame(2, $payload['totals']['students']);
$this->assertSame(6, $payload['totals']['primary']);
$this->assertSame(1, $payload['totals']['secondary']);
}
public function test_list_for_class_limits_results(): void
{
$this->seedStickerData();
$service = new StickerCountService();
$payload = $service->listForClass('2025-2026', 'Fall', 101);
$this->assertSame(1, $payload['totals']['students']);
$this->assertSame(3, $payload['totals']['primary']);
$this->assertSame(0, $payload['totals']['secondary']);
}
private function seedStickerData(): void
{
DB::table('configuration')->insert([
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
]);
DB::table('classes')->insert([
[
'id' => 1,
'class_name' => 'Class 1',
'schedule' => 'Sun',
'capacity' => 20,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
[
'id' => 2,
'class_name' => 'Class 2',
'schedule' => 'Sun',
'capacity' => 20,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
[
'id' => 3,
'class_name' => 'Class 3',
'schedule' => 'Sun',
'capacity' => 20,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
]);
DB::table('classSection')->insert([
[
'id' => 1,
'class_id' => 1,
'class_section_id' => 101,
'class_section_name' => '1-A',
'semester' => 'Fall',
'school_year' => '2025-2026',
],
[
'id' => 2,
'class_id' => 2,
'class_section_id' => 102,
'class_section_name' => '5-B',
'semester' => 'Fall',
'school_year' => '2025-2026',
],
[
'id' => 3,
'class_id' => 3,
'class_section_id' => 103,
'class_section_name' => 'Youth-1',
'semester' => 'Fall',
'school_year' => '2025-2026',
],
]);
DB::table('students')->insert([
[
'school_id' => 'S-1',
'firstname' => 'Student',
'lastname' => 'One',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
'semester' => 'Fall',
'is_active' => 1,
'is_new' => 0,
],
[
'school_id' => 'S-2',
'firstname' => 'Student',
'lastname' => 'Two',
'age' => 10,
'gender' => 'Female',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
'semester' => 'Fall',
'is_active' => 1,
'is_new' => 0,
],
[
'school_id' => 'S-3',
'firstname' => 'Student',
'lastname' => 'Three',
'age' => 12,
'gender' => 'Female',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
'semester' => 'Fall',
'is_active' => 1,
'is_new' => 1,
],
]);
DB::table('student_class')->insert([
[
'student_id' => 1,
'class_section_id' => 101,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
[
'student_id' => 2,
'class_section_id' => 102,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
[
'student_id' => 3,
'class_section_id' => 103,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
]);
}
}
@@ -0,0 +1,30 @@
<?php
namespace Tests\Unit\Services\ClassPreparation;
use App\Services\ClassPreparation\ClassPreparationAdjustmentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ClassPreparationAdjustmentServiceTest extends TestCase
{
use RefreshDatabase;
public function test_apply_adjustments_updates_counts(): void
{
DB::table('class_prep_adjustments')->insert([
'class_section_id' => 101,
'item_name' => 'Small Table',
'adjustment' => 2,
'school_year' => '2025-2026',
'adjustable' => 1,
]);
$service = new ClassPreparationAdjustmentService();
[$items, $adjMap] = $service->applyAdjustments(['Small Table' => 1], '101', '2025-2026');
$this->assertSame(3, $items['Small Table']);
$this->assertSame(2, $adjMap['Small Table']);
}
}
@@ -0,0 +1,59 @@
<?php
namespace Tests\Unit\Services\ClassPreparation;
use App\Services\ClassPreparation\ClassPreparationCalculatorService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ClassPreparationCalculatorServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_class_level_by_section(): void
{
DB::table('classSection')->insert([
'id' => 1,
'class_id' => 1,
'class_section_id' => 101,
'class_section_name' => '2-A',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$service = new ClassPreparationCalculatorService();
$this->assertSame(2, $service->getClassLevelBySection('101'));
$this->assertSame(1, $service->getClassLevelBySection('KG'));
}
public function test_calculate_prep_items_for_lower_grades(): void
{
DB::table('configuration')->insert([
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
]);
DB::table('inventory_categories')->insert([
['name' => 'Small Table', 'grade_min' => null, 'grade_max' => null, 'type' => 'classroom'],
['name' => 'Small Chair', 'grade_min' => null, 'grade_max' => null, 'type' => 'classroom'],
['name' => 'Teacher Chair', 'grade_min' => null, 'grade_max' => null, 'type' => 'classroom'],
['name' => 'Grade Box', 'grade_min' => null, 'grade_max' => null, 'type' => 'classroom'],
]);
DB::table('teacher_class')->insert([
'class_section_id' => 101,
'teacher_id' => 1,
'position' => 'main',
'school_year' => '2025-2026',
]);
$service = new ClassPreparationCalculatorService();
$items = $service->calculatePrepItems(6, 1, '101');
$this->assertSame(2, $items['Small Table']);
$this->assertSame(6, $items['Small Chair']);
$this->assertSame(1, $items['Teacher Chair']);
$this->assertSame(1, $items['Grade Box']);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Tests\Unit\Services\ClassPreparation;
use App\Services\ClassPreparation\ClassPreparationContextService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ClassPreparationContextServiceTest extends TestCase
{
use RefreshDatabase;
public function test_has_roster_for_semester(): void
{
DB::table('student_class')->insert([
'student_id' => 1,
'class_section_id' => 101,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$service = new ClassPreparationContextService();
$this->assertTrue($service->hasRosterForSemester('2025-2026', 'Fall'));
$this->assertFalse($service->hasRosterForSemester('2025-2026', 'Spring'));
}
}
@@ -0,0 +1,48 @@
<?php
namespace Tests\Unit\Services\ClassPreparation;
use App\Services\ClassPreparation\ClassPreparationInventoryService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ClassPreparationInventoryServiceTest extends TestCase
{
use RefreshDatabase;
public function test_build_availability_uses_max_value(): void
{
DB::table('inventory_categories')->insert([
'id' => 1,
'type' => 'classroom',
'name' => 'Small Table',
]);
DB::table('inventory_items')->insert([
[
'type' => 'classroom',
'category_id' => 1,
'name' => 'Small Table',
'quantity' => 5,
'good_qty' => 5,
'school_year' => '2025-2026',
'semester' => 'Fall',
],
[
'type' => 'classroom',
'category_id' => 1,
'name' => 'Small Table',
'quantity' => 3,
'good_qty' => 3,
'school_year' => '2025-2026',
'semester' => 'Fall',
],
]);
$service = new ClassPreparationInventoryService();
$map = $service->buildAvailability('2025-2026', 'Fall', true, ['Small Table']);
$this->assertSame(8, $map['Small Table']);
}
}
@@ -0,0 +1,36 @@
<?php
namespace Tests\Unit\Services\ClassPreparation;
use App\Services\ClassPreparation\ClassPreparationLogService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ClassPreparationLogServiceTest extends TestCase
{
use RefreshDatabase;
public function test_has_prep_changed_detects_diff(): void
{
$service = new ClassPreparationLogService();
$this->assertTrue($service->hasPrepChanged(['a' => 1], ['a' => 2]));
$this->assertFalse($service->hasPrepChanged(['a' => 1], ['a' => 1]));
}
public function test_create_log_and_get_latest(): void
{
$service = new ClassPreparationLogService();
$created = $service->createLog('101', '1-A', '2025-2026', ['Small Table' => 2], '2025-09-01 00:00:00');
$this->assertTrue($created);
$this->assertDatabaseHas('class_preparation_log', [
'class_section_id' => 101,
'school_year' => '2025-2026',
]);
$log = $service->getLatestLog('101', '2025-2026');
$this->assertNotNull($log);
}
}
@@ -0,0 +1,84 @@
<?php
namespace Tests\Unit\Services\ClassPreparation;
use App\Services\ClassPreparation\ClassPreparationRosterService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ClassPreparationRosterServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_class_section_student_counts(): void
{
$this->seedRoster();
$service = new ClassPreparationRosterService();
$rows = $service->getClassSectionStudentCounts('2025-2026', 'Fall', true);
$this->assertCount(1, $rows);
$this->assertSame(101, (int) $rows[0]->class_section_id);
$this->assertSame(2, (int) $rows[0]->student_count);
}
public function test_get_student_count_for_section(): void
{
$this->seedRoster();
$service = new ClassPreparationRosterService();
$count = $service->getStudentCountForSection('2025-2026', 'Fall', true, '101');
$this->assertSame(2, $count);
}
private function seedRoster(): void
{
DB::table('students')->insert([
[
'id' => 1,
'school_id' => 'S-1',
'firstname' => 'Student',
'lastname' => 'One',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
'semester' => 'Fall',
'is_active' => 1,
],
[
'id' => 2,
'school_id' => 'S-2',
'firstname' => 'Student',
'lastname' => 'Two',
'age' => 9,
'gender' => 'Female',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
'semester' => 'Fall',
'is_active' => 1,
],
]);
DB::table('student_class')->insert([
[
'student_id' => 1,
'class_section_id' => 101,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
[
'student_id' => 2,
'class_section_id' => 101,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
]);
}
}
@@ -0,0 +1,93 @@
<?php
namespace Tests\Unit\Services\ClassPreparation;
use App\Services\ClassPreparation\ClassPreparationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ClassPreparationServiceTest extends TestCase
{
use RefreshDatabase;
public function test_list_prep_returns_sections_and_totals(): void
{
$this->seedPrepData();
$service = new ClassPreparationService();
$payload = $service->listPrep('2025-2026', 'Fall');
$this->assertSame('2025-2026', $payload['schoolYear']);
$this->assertSame('Fall', $payload['semester']);
$this->assertNotEmpty($payload['results']);
$this->assertArrayHasKey('Small Table', $payload['totals']);
}
public function test_mark_printed_creates_logs(): void
{
$this->seedPrepData();
$service = new ClassPreparationService();
$count = $service->markPrinted('2025-2026', 'Fall', [101]);
$this->assertSame(1, $count);
$this->assertDatabaseHas('class_preparation_log', [
'class_section_id' => 101,
'school_year' => '2025-2026',
]);
}
private function seedPrepData(): void
{
DB::table('configuration')->insert([
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
]);
DB::table('classSection')->insert([
'id' => 1,
'class_id' => 1,
'class_section_id' => 101,
'class_section_name' => '1-A',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('students')->insert([
'school_id' => 'S-1',
'firstname' => 'Student',
'lastname' => 'One',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
'semester' => 'Fall',
'is_active' => 1,
]);
DB::table('student_class')->insert([
'student_id' => 1,
'class_section_id' => 101,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('inventory_categories')->insert([
'type' => 'classroom',
'name' => 'Small Table',
]);
DB::table('inventory_items')->insert([
'type' => 'classroom',
'category_id' => 1,
'name' => 'Small Table',
'quantity' => 10,
'good_qty' => 10,
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
}
}
@@ -0,0 +1,31 @@
<?php
namespace Tests\Unit\Services\Communication;
use App\Services\Communication\CommunicationTemplateService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class CommunicationTemplateServiceTest extends TestCase
{
use RefreshDatabase;
public function test_list_active_templates_maps_columns(): void
{
DB::table('email_templates')->insert([
'id' => 1,
'code' => 'welcome',
'variant' => 'default',
'subject' => 'Hello',
'body_html' => 'Body',
'is_active' => 1,
]);
$service = new CommunicationTemplateService();
$templates = $service->listActiveTemplates();
$this->assertSame('welcome', $templates[0]['template_key']);
$this->assertSame('Body', $templates[0]['body']);
}
}
@@ -0,0 +1,46 @@
<?php
namespace Tests\Unit\Services\CompetitionScores;
use App\Models\CompetitionScore;
use App\Services\CompetitionScores\CompetitionScoresSaveService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class CompetitionScoresSaveServiceTest extends TestCase
{
use RefreshDatabase;
public function test_filter_scores_rejects_non_integers(): void
{
$service = new CompetitionScoresSaveService();
[$clean, $invalid] = $service->filterScores([
1 => '10',
2 => '4.5',
]);
$this->assertSame([1 => 10], $clean);
$this->assertSame([2], $invalid);
}
public function test_save_scores_upserts(): void
{
DB::table('competition_scores')->insert([
'competition_id' => 1,
'student_id' => 1,
'class_section_id' => 101,
'score' => 2,
]);
$service = new CompetitionScoresSaveService();
$service->saveScores(1, 101, [1 => 7]);
$this->assertDatabaseHas('competition_scores', [
'competition_id' => 1,
'student_id' => 1,
'class_section_id' => 101,
'score' => 7,
]);
}
}
@@ -0,0 +1,38 @@
<?php
namespace Tests\Unit\Services\Discounts;
use App\Services\Discounts\DiscountApplyService;
use App\Services\Discounts\DiscountContextService;
use App\Services\Discounts\DiscountInvoiceService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class DiscountApplyServiceTest extends TestCase
{
use RefreshDatabase;
public function test_apply_voucher_returns_error_when_no_remaining_uses(): void
{
DB::table('configuration')->insert([
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
]);
DB::table('discount_vouchers')->insert([
'id' => 1,
'code' => 'USED-OUT',
'discount_type' => 'fixed',
'discount_value' => 10,
'max_uses' => 1,
'times_used' => 1,
'is_active' => 1,
]);
$service = new DiscountApplyService(new DiscountContextService(), new DiscountInvoiceService());
$result = $service->applyVoucher(1, [10], true, 1);
$this->assertFalse($result['ok']);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Tests\Unit\Services\Expenses;
use App\Services\Expenses\ExpenseReceiptService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class ExpenseReceiptServiceTest extends TestCase
{
use RefreshDatabase;
public function test_store_receipt_returns_filename_and_url(): void
{
Storage::fake();
$service = new ExpenseReceiptService();
$file = UploadedFile::fake()->create('receipt.pdf', 10, 'application/pdf');
$filename = $service->storeReceipt($file);
$this->assertNotEmpty($filename);
$this->assertSame(url('receipts/' . $filename), $service->receiptUrl($filename));
}
}
@@ -0,0 +1,75 @@
<?php
namespace Tests\Unit\Services\Expenses;
use App\Services\Expenses\ExpenseStaffService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ExpenseStaffServiceTest extends TestCase
{
use RefreshDatabase;
public function test_list_staff_users_excludes_parents(): void
{
DB::table('roles')->insert([
['id' => 1, 'name' => 'administrator', 'slug' => 'administrator', 'is_active' => 1],
['id' => 2, 'name' => 'parent', 'slug' => 'parent', 'is_active' => 1],
]);
DB::table('users')->insert([
[
'id' => 10,
'school_id' => 1,
'firstname' => 'Staff',
'lastname' => 'User',
'cellphone' => '5555555555',
'email' => 'staff@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',
],
[
'id' => 11,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '5555555555',
'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],
['user_id' => 11, 'role_id' => 2],
]);
$service = new ExpenseStaffService();
$staff = $service->listStaffUsers();
$this->assertCount(1, $staff);
$this->assertSame(10, $staff[0]['id']);
}
}
@@ -0,0 +1,61 @@
<?php
namespace Tests\Unit\Services\ExtraCharges;
use App\Services\ExtraCharges\ExtraChargesMetaService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ExtraChargesMetaServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_school_years_uses_fallback(): void
{
$service = new ExtraChargesMetaService();
$years = $service->getSchoolYears('2025-2026');
$this->assertSame(['2025-2026', '2024-2025', '2023-2024', '2022-2023'], $years);
}
public function test_get_school_years_merges_sources(): void
{
DB::table('additional_charges')->insert([
[
'parent_id' => 1,
'school_year' => '2025-2026',
'semester' => 'Fall',
'charge_type' => 'add',
'title' => 'Test',
'amount' => 1,
],
[
'parent_id' => 1,
'school_year' => '2024-2025',
'semester' => 'Fall',
'charge_type' => 'add',
'title' => 'Test',
'amount' => 1,
],
]);
DB::table('invoices')->insert([
[
'parent_id' => 1,
'invoice_number' => 'INV-1',
'total_amount' => 10,
'balance' => 10,
'paid_amount' => 0,
'issue_date' => '2025-09-01',
'status' => 'Unpaid',
'school_year' => '2023-2024',
],
]);
$service = new ExtraChargesMetaService();
$years = $service->getSchoolYears();
$this->assertSame(['2025-2026', '2024-2025', '2023-2024'], $years);
}
}
@@ -0,0 +1,43 @@
<?php
namespace Tests\Unit\Services\Files;
use App\Services\Files\ExamDraftDownloadNameService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ExamDraftDownloadNameServiceTest extends TestCase
{
use RefreshDatabase;
public function test_build_returns_slugified_name(): void
{
DB::table('classSection')->insert([
'id' => 1,
'class_section_id' => 1,
'class_section_name' => '2B',
'class_id' => 1,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('exam_drafts')->insert([
'id' => 1,
'teacher_id' => 1,
'class_section_id' => 1,
'semester' => 'Fall',
'school_year' => '2025-2026',
'exam_type' => 'Final Exam',
'draft_title' => 'Draft',
'teacher_file' => 'draft2.pdf',
'version' => 3,
'status' => 'draft',
]);
$service = new ExamDraftDownloadNameService();
$name = $service->build('draft2.pdf', 'drafts');
$this->assertSame('2b_final_exam_v3', $name);
}
}
@@ -0,0 +1,69 @@
<?php
namespace Tests\Unit\Services\Files;
use App\Services\Files\FileServeService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\TestCase;
class FileServeServiceTest extends TestCase
{
use RefreshDatabase;
public function test_meta_returns_file_metadata(): void
{
$dir = storage_path('testing/files');
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$path = $dir . DIRECTORY_SEPARATOR . 'sample.pdf';
file_put_contents($path, 'PDFDATA');
$service = new FileServeService();
$meta = $service->meta($dir, 'sample.pdf', ['pdf'], 'download');
$this->assertSame('sample.pdf', $meta['name']);
$this->assertSame('download.pdf', $meta['download_name']);
$this->assertSame(7, $meta['size']);
$this->assertNotEmpty($meta['etag']);
}
public function test_serve_inline_returns_304_when_etag_matches(): void
{
$dir = storage_path('testing/files');
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$path = $dir . DIRECTORY_SEPARATOR . 'cached.pdf';
file_put_contents($path, 'PDFDATA');
$service = new FileServeService();
$meta = $service->meta($dir, 'cached.pdf', ['pdf']);
$request = Request::create('/files/cached.pdf', 'GET', [], [], [], [
'HTTP_IF_NONE_MATCH' => $meta['etag'],
]);
$response = $service->serveInline($dir, 'cached.pdf', ['pdf'], $request);
$this->assertSame(304, $response->getStatusCode());
$this->assertSame($meta['etag'], $response->headers->get('ETag'));
}
public function test_serve_inline_rejects_invalid_extension(): void
{
$this->expectException(HttpException::class);
$dir = storage_path('testing/files');
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$path = $dir . DIRECTORY_SEPARATOR . 'sample.exe';
file_put_contents($path, 'DATA');
$service = new FileServeService();
$service->meta($dir, 'sample.exe', ['pdf']);
}
}
@@ -0,0 +1,31 @@
<?php
namespace Tests\Unit\Services\Finance;
use App\Services\Finance\FinancialChartService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class FinancialChartServiceTest extends TestCase
{
use RefreshDatabase;
public function test_generate_charts_returns_null_in_tests(): void
{
$service = new FinancialChartService();
$summary = [
'totalCharges' => 100,
'amountCollected' => 80,
'totalUnpaid' => 20,
'totalDiscounts' => 5,
'totalRefunds' => 2,
'totalExpenses' => 10,
'totalReimbursements' => 4,
'netAmount' => 93,
];
$this->assertNull($service->generateBarChart($summary));
$this->assertNull($service->generatePieChart($summary));
}
}
@@ -0,0 +1,94 @@
<?php
namespace Tests\Unit\Services\Finance;
use App\Services\Finance\FinancialPaymentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class FinancialPaymentServiceTest extends TestCase
{
use RefreshDatabase;
public function test_payment_aggregates_exclude_void_statuses(): void
{
DB::table('payments')->insert([
[
'id' => 1,
'parent_id' => 10,
'invoice_id' => 1,
'total_amount' => 100,
'paid_amount' => 30,
'balance' => 70,
'number_of_installments' => 1,
'payment_method' => 'Cash',
'payment_date' => '2025-01-05',
'school_year' => '2025-2026',
'status' => 'Paid',
],
[
'id' => 2,
'parent_id' => 10,
'invoice_id' => 1,
'total_amount' => 100,
'paid_amount' => 20,
'balance' => 50,
'number_of_installments' => 1,
'payment_method' => 'Credit Card',
'payment_date' => '2025-01-06',
'school_year' => '2025-2026',
'status' => 'Paid',
],
[
'id' => 3,
'parent_id' => 10,
'invoice_id' => 1,
'total_amount' => 100,
'paid_amount' => 50,
'balance' => 0,
'number_of_installments' => 1,
'payment_method' => 'Cash',
'payment_date' => '2025-01-07',
'school_year' => '2025-2026',
'status' => 'void',
],
[
'id' => 4,
'parent_id' => 11,
'invoice_id' => 2,
'total_amount' => 40,
'paid_amount' => 10,
'balance' => 30,
'number_of_installments' => 1,
'payment_method' => 'Check',
'payment_date' => '2025-01-08',
'school_year' => '2025-2026',
'status' => 'Paid',
],
]);
$service = new FinancialPaymentService();
$payments = $service->paymentsByInvoice('2025-2026', '2025-01-01', '2025-01-31');
$breakdown = $service->paymentBreakdown('2025-2026', '2025-01-01', '2025-01-31');
$totals = $service->paymentTotals('2025-2026', '2025-01-01', '2025-01-31');
$map = [];
foreach ($payments as $row) {
$map[(int) $row['invoice_id']] = (float) $row['paid_amount'];
}
$this->assertSame(50.0, $map[1]);
$this->assertSame(10.0, $map[2]);
$this->assertSame(30.0, (float) ($breakdown[1]['cash'] ?? 0));
$this->assertSame(20.0, (float) ($breakdown[1]['credit'] ?? 0));
$this->assertSame(10.0, (float) ($breakdown[2]['check'] ?? 0));
$this->assertSame(60.0, $totals['total_all']);
$this->assertSame(30.0, $totals['total_cash']);
$this->assertSame(20.0, $totals['total_credit']);
$this->assertSame(10.0, $totals['total_check']);
}
}
@@ -0,0 +1,37 @@
<?php
namespace Tests\Unit\Services\Finance;
use App\Services\Finance\FinancialChartService;
use App\Services\Finance\FinancialPdfReportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class FinancialPdfReportServiceTest extends TestCase
{
use RefreshDatabase;
public function test_build_pdf_returns_bytes(): void
{
$service = new FinancialPdfReportService(new FinancialChartService());
$summary = [
'schoolYear' => '2025-2026',
'totalCharges' => 100,
'totalExtraCharges' => 20,
'totalDiscounts' => 10,
'totalRefunds' => 5,
'totalExpenses' => 15,
'totalReimbursements' => 8,
'donationToSchool' => 3,
'netAmount' => 85,
'amountCollected' => 60,
'totalUnpaid' => 40,
];
$pdf = $service->buildPdf($summary);
$this->assertIsString($pdf);
$this->assertNotSame('', $pdf);
}
}
@@ -0,0 +1,126 @@
<?php
namespace Tests\Unit\Services\Finance;
use App\Services\Finance\FinancialPaymentService;
use App\Services\Finance\FinancialReportService;
use App\Services\Finance\FinancialSchoolYearService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class FinancialReportServiceTest extends TestCase
{
use RefreshDatabase;
public function test_report_returns_grouped_data(): void
{
DB::table('users')->insert([
'id' => 10,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '5555555555',
'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('invoices')->insert([
'id' => 1,
'parent_id' => 10,
'invoice_number' => 'INV-001',
'total_amount' => 100,
'balance' => 40,
'paid_amount' => 60,
'has_discount' => 1,
'issue_date' => '2025-01-01',
'due_date' => '2025-02-01',
'status' => 'unpaid',
'school_year' => '2025-2026',
]);
DB::table('payments')->insert([
'id' => 1,
'parent_id' => 10,
'invoice_id' => 1,
'total_amount' => 100,
'paid_amount' => 60,
'balance' => 40,
'number_of_installments' => 1,
'payment_method' => 'Cash',
'payment_date' => '2025-01-02',
'school_year' => '2025-2026',
'status' => 'Paid',
]);
DB::table('refunds')->insert([
'id' => 1,
'parent_id' => 10,
'school_year' => '2025-2026',
'invoice_id' => 1,
'refund_amount' => 5,
'status' => 'Paid',
'refund_paid_amount' => 5,
]);
DB::table('discount_usages')->insert([
'id' => 1,
'voucher_id' => 1,
'invoice_id' => 1,
'parent_id' => 10,
'discount_amount' => 10,
'school_year' => '2025-2026',
]);
DB::table('expenses')->insert([
'id' => 1,
'category' => 'Expense',
'amount' => 20,
'date_of_purchase' => '2025-01-05',
'purchased_by' => 10,
'added_by' => 1,
'school_year' => '2025-2026',
'semester' => 'Fall',
'status' => 'approved',
]);
DB::table('reimbursements')->insert([
'id' => 1,
'expense_id' => 1,
'amount' => 15,
'reimbursed_to' => 10,
'added_by' => 1,
'status' => 'Paid',
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
$service = new FinancialReportService(
new FinancialPaymentService(),
new FinancialSchoolYearService()
);
$report = $service->getReport('2025-01-01', '2025-12-31', '2025-2026');
$this->assertSame('2025-2026', $report['selectedYear']);
$this->assertSame(['2025-2026'], $report['schoolYears']);
$this->assertCount(1, $report['invoices']);
$this->assertCount(1, $report['payments']);
$this->assertCount(1, $report['refunds']);
$this->assertCount(1, $report['discounts']);
$this->assertCount(1, $report['expenses']);
$this->assertCount(1, $report['reimbursements']);
$this->assertSame(60.0, $report['paymentTotals']['total_all']);
}
}
@@ -0,0 +1,50 @@
<?php
namespace Tests\Unit\Services\Finance;
use App\Services\Finance\FinancialSchoolYearService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class FinancialSchoolYearServiceTest extends TestCase
{
use RefreshDatabase;
public function test_list_years_returns_distinct_sorted_years(): void
{
DB::table('invoices')->insert([
[
'id' => 1,
'parent_id' => 1,
'invoice_number' => 'INV-100',
'total_amount' => 10,
'balance' => 10,
'paid_amount' => 0,
'has_discount' => 0,
'issue_date' => '2024-01-01',
'due_date' => '2024-02-01',
'status' => 'unpaid',
'school_year' => '2024-2025',
],
[
'id' => 2,
'parent_id' => 1,
'invoice_number' => 'INV-101',
'total_amount' => 10,
'balance' => 10,
'paid_amount' => 0,
'has_discount' => 0,
'issue_date' => '2025-01-01',
'due_date' => '2025-02-01',
'status' => 'unpaid',
'school_year' => '2025-2026',
],
]);
$service = new FinancialSchoolYearService();
$years = $service->listYears();
$this->assertSame(['2025-2026', '2024-2025'], $years);
}
}
@@ -0,0 +1,165 @@
<?php
namespace Tests\Unit\Services\Finance;
use App\Services\Finance\FinancialDonationRecipientService;
use App\Services\Finance\FinancialSummaryService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class FinancialSummaryServiceTest extends TestCase
{
use RefreshDatabase;
public function test_summary_calculates_totals(): void
{
DB::table('users')->insert([
'id' => 2,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '5555555555',
'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('invoices')->insert([
'id' => 1,
'parent_id' => 2,
'invoice_number' => 'INV-001',
'total_amount' => 100,
'balance' => 40,
'paid_amount' => 60,
'has_discount' => 1,
'issue_date' => '2025-01-10',
'due_date' => '2025-02-10',
'status' => 'unpaid',
'school_year' => '2025-2026',
]);
DB::table('payments')->insert([
'id' => 1,
'parent_id' => 2,
'invoice_id' => 1,
'total_amount' => 100,
'paid_amount' => 60,
'balance' => 40,
'number_of_installments' => 1,
'payment_method' => 'Cash',
'payment_date' => '2025-01-15',
'school_year' => '2025-2026',
'status' => 'Paid',
]);
DB::table('discount_usages')->insert([
'id' => 1,
'voucher_id' => 1,
'invoice_id' => 1,
'parent_id' => 2,
'discount_amount' => 10,
'school_year' => '2025-2026',
]);
DB::table('refunds')->insert([
'id' => 1,
'parent_id' => 2,
'school_year' => '2025-2026',
'invoice_id' => 1,
'refund_amount' => 5,
'status' => 'Paid',
'refund_paid_amount' => 5,
]);
DB::table('additional_charges')->insert([
[
'id' => 1,
'parent_id' => 2,
'amount' => 20,
'school_year' => '2025-2026',
'status' => 'pending',
],
[
'id' => 2,
'parent_id' => 2,
'amount' => 30,
'invoice_id' => 1,
'school_year' => '2025-2026',
'status' => 'applied',
],
]);
DB::table('expenses')->insert([
[
'id' => 1,
'category' => 'Donation',
'amount' => 12,
'date_of_purchase' => '2025-01-05',
'purchased_by' => 2,
'added_by' => 1,
'school_year' => '2025-2026',
'semester' => 'Fall',
'status' => 'approved',
],
[
'id' => 2,
'category' => 'Expense',
'amount' => 8,
'date_of_purchase' => '2025-01-06',
'purchased_by' => 2,
'added_by' => 1,
'school_year' => '2025-2026',
'semester' => 'Fall',
'status' => 'approved',
],
]);
DB::table('reimbursements')->insert([
[
'id' => 1,
'expense_id' => 2,
'amount' => 15,
'reimbursed_to' => 2,
'added_by' => 1,
'status' => 'Paid',
'school_year' => '2025-2026',
'semester' => 'Fall',
],
[
'id' => 2,
'expense_id' => 1,
'amount' => 7,
'reimbursed_to' => 990002,
'added_by' => 1,
'status' => 'Paid',
'school_year' => '2025-2026',
'semester' => 'Fall',
],
]);
$service = new FinancialSummaryService(new FinancialDonationRecipientService());
$summary = $service->getSummary('2025-01-01', '2025-12-31', '2025-2026');
$this->assertSame(120.0, $summary['totalCharges']);
$this->assertSame(50.0, $summary['totalExtraCharges']);
$this->assertSame(10.0, $summary['totalDiscounts']);
$this->assertSame(5.0, $summary['totalRefunds']);
$this->assertSame(20.0, $summary['totalExpenses']);
$this->assertSame(15.0, $summary['totalReimbursements']);
$this->assertSame(19.0, $summary['donationToSchool']);
$this->assertSame(60.0, $summary['totalPaid']);
$this->assertSame(45.0, $summary['totalUnpaid']);
$this->assertSame(105.0, $summary['netAmount']);
}
}
@@ -0,0 +1,110 @@
<?php
namespace Tests\Unit\Services\Finance;
use App\Services\Finance\FinancialSchoolYearService;
use App\Services\Finance\FinancialUnpaidParentsService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class FinancialUnpaidParentsServiceTest extends TestCase
{
use RefreshDatabase;
public function test_unpaid_parents_returns_installment_data(): void
{
DB::table('configuration')->insert([
['id' => 1, 'config_key' => 'installment_date', 'config_value' => date('Y-m-t')],
]);
DB::table('users')->insert([
'id' => 2,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '5555555555',
'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('invoices')->insert([
[
'id' => 1,
'parent_id' => 2,
'invoice_number' => 'INV-001',
'total_amount' => 100,
'balance' => 40,
'paid_amount' => 60,
'has_discount' => 1,
'issue_date' => '2025-01-10',
'due_date' => '2025-02-10',
'status' => 'unpaid',
'school_year' => '2025-2026',
],
]);
DB::table('payments')->insert([
'id' => 1,
'parent_id' => 2,
'invoice_id' => 1,
'total_amount' => 100,
'paid_amount' => 60,
'balance' => 40,
'number_of_installments' => 1,
'payment_method' => 'Cash',
'payment_date' => '2025-01-15',
'school_year' => '2025-2026',
'status' => 'Paid',
]);
DB::table('discount_usages')->insert([
'id' => 1,
'voucher_id' => 1,
'invoice_id' => 1,
'parent_id' => 2,
'discount_amount' => 10,
'school_year' => '2025-2026',
]);
DB::table('refunds')->insert([
'id' => 1,
'parent_id' => 2,
'school_year' => '2025-2026',
'invoice_id' => 1,
'refund_amount' => 5,
'status' => 'Paid',
'refund_paid_amount' => 5,
]);
DB::table('additional_charges')->insert([
'id' => 1,
'parent_id' => 2,
'amount' => 20,
'school_year' => '2025-2026',
'status' => 'pending',
]);
$service = new FinancialUnpaidParentsService(new FinancialSchoolYearService());
$result = $service->getUnpaidParents('2025-2026');
$this->assertSame('2025-2026', $result['schoolYear']);
$this->assertCount(1, $result['results']);
$row = $result['results'][0];
$this->assertSame(45.0, $row['total_balance']);
$this->assertSame('installment', $row['type']);
$this->assertSame(1, $row['has_installment']);
}
}
@@ -0,0 +1,55 @@
<?php
namespace Tests\Unit\Services\Grading;
use App\Services\Grading\GradingBelowSixtyService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class GradingBelowSixtyServiceTest extends TestCase
{
use RefreshDatabase;
public function test_list_rows_returns_students_below_sixty(): void
{
DB::table('students')->insert([
'id' => 100,
'parent_id' => 10,
'firstname' => 'Kid',
'lastname' => 'User',
'school_id' => 1,
'age' => 10,
'gender' => 'M',
'photo_consent' => 1,
'year_of_registration' => '2025',
'is_active' => 1,
]);
DB::table('classSection')->insert([
'id' => 1,
'class_section_id' => 1,
'class_section_name' => '1A',
'class_id' => 1,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('semester_scores')->insert([
'id' => 1,
'student_id' => 100,
'school_id' => 1,
'class_section_id' => 1,
'semester' => 'Fall',
'school_year' => '2025-2026',
'semester_score' => 55,
]);
$service = new GradingBelowSixtyService();
$rows = $service->listRows('2025-2026', 'Fall');
$this->assertCount(1, $rows);
$this->assertSame(100, (int) $rows[0]['student_id']);
$this->assertSame('Open', $rows[0]['status']);
}
}
@@ -0,0 +1,69 @@
<?php
namespace Tests\Unit\Services\Grading;
use App\Services\Grading\GradingLockService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class GradingLockServiceTest extends TestCase
{
use RefreshDatabase;
public function test_toggle_creates_lock(): void
{
DB::table('classSection')->insert([
'id' => 1,
'class_section_id' => 1,
'class_section_name' => '1A',
'class_id' => 1,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$service = new GradingLockService();
$locked = $service->toggle(1, 'Fall', '2025-2026', 99);
$this->assertTrue($locked);
$this->assertDatabaseHas('grading_locks', [
'class_section_id' => 1,
'semester' => 'Fall',
'school_year' => '2025-2026',
'is_locked' => 1,
]);
}
public function test_lock_all_locks_sections(): void
{
DB::table('classSection')->insert([
[
'id' => 1,
'class_section_id' => 1,
'class_section_name' => '1A',
'class_id' => 1,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
[
'id' => 2,
'class_section_id' => 2,
'class_section_name' => '1B',
'class_id' => 1,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
]);
$service = new GradingLockService();
$count = $service->lockAll('Fall', '2025-2026', 99);
$this->assertSame(2, $count);
$this->assertDatabaseHas('grading_locks', [
'class_section_id' => 2,
'semester' => 'Fall',
'school_year' => '2025-2026',
'is_locked' => 1,
]);
}
}
@@ -0,0 +1,81 @@
<?php
namespace Tests\Unit\Services\Grading;
use App\Services\Grading\HomeworkTrackingCalendarService;
use App\Services\Grading\HomeworkTrackingService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class HomeworkTrackingServiceTest extends TestCase
{
use RefreshDatabase;
public function test_report_returns_teacher_rows(): void
{
DB::table('configuration')->insert([
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
]);
DB::table('classSection')->insert([
'id' => 1,
'class_section_id' => 1,
'class_section_name' => '1A',
'class_id' => 1,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Teacher',
'lastname' => 'User',
'cellphone' => '5555555555',
'email' => 'teacher@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('teacher_class')->insert([
'id' => 1,
'class_section_id' => 1,
'teacher_id' => 1,
'position' => 'main',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('homework')->insert([
'id' => 1,
'student_id' => 100,
'school_id' => 1,
'class_section_id' => 1,
'updated_by' => 1,
'homework_index' => 1,
'score' => 90,
'semester' => 'Fall',
'school_year' => '2025-2026',
'created_at' => now(),
'updated_at' => now(),
]);
$service = new HomeworkTrackingService(new HomeworkTrackingCalendarService());
$result = $service->report('Fall', '2025-2026', 1);
$this->assertNotEmpty($result['teachers']);
$this->assertSame(1, $result['teachers'][0]['class_section_id']);
}
}
@@ -0,0 +1,62 @@
<?php
namespace Tests\Unit\Services\Incidents;
use App\Services\Incidents\IncidentAnalysisService;
use App\Services\Incidents\IncidentLookupService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class IncidentAnalysisServiceTest extends TestCase
{
use RefreshDatabase;
public function test_analyze_groups_incidents_by_student(): void
{
DB::table('classSection')->insert([
'id' => 1,
'class_section_id' => 1,
'class_section_name' => '1A',
'class_id' => 1,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('incident')->insert([
[
'id' => 1,
'student_id' => 10,
'student_name' => 'Kid Tester',
'grade' => '1',
'incident' => 'behavior',
'incident_datetime' => '2025-01-02 10:00:00',
'incident_state' => 'Closed',
'semester' => 'Fall',
'school_year' => '2025-2026',
],
[
'id' => 2,
'student_id' => 10,
'student_name' => 'Kid Tester',
'grade' => '1',
'incident' => 'behavior',
'incident_datetime' => '2025-01-01 10:00:00',
'incident_state' => 'Canceled',
'semester' => 'Fall',
'school_year' => '2025-2026',
],
]);
$service = new IncidentAnalysisService(new IncidentLookupService());
$result = $service->analyze('2025-2026', 'Fall');
$this->assertCount(1, $result);
$this->assertSame('Kid Tester', $result[0]['student_name']);
$this->assertSame('1A', $result[0]['grade']);
$this->assertSame(2, $result[0]['total']);
$this->assertSame(1, $result[0]['closed']);
$this->assertSame(1, $result[0]['canceled']);
$this->assertSame('2025-01-02 10:00:00', $result[0]['last_incident']);
}
}
@@ -0,0 +1,66 @@
<?php
namespace Tests\Unit\Services\Incidents;
use App\Services\Incidents\IncidentHistoryService;
use App\Services\Incidents\IncidentLookupService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class IncidentHistoryServiceTest extends TestCase
{
use RefreshDatabase;
public function test_processed_enriches_grade_and_updater_names(): void
{
DB::table('classSection')->insert([
'id' => 1,
'class_id' => 1,
'class_section_id' => 5,
'class_section_name' => '5A',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('users')->insert([
'id' => 3,
'school_id' => 1,
'firstname' => 'Admin',
'lastname' => 'User',
'cellphone' => '5555555555',
'email' => 'admin@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('incident')->insert([
'student_id' => 1,
'student_name' => 'Student One',
'grade' => 5,
'incident' => 'Test incident',
'incident_datetime' => '2025-01-01 10:00:00',
'incident_state' => 'open',
'updated_by_open' => 3,
'open_description' => 'Opened',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$service = new IncidentHistoryService(new IncidentLookupService());
$rows = $service->processed('2025-2026', 'Fall');
$this->assertSame('5A', $rows[0]['grade']);
$this->assertSame('Admin User', $rows[0]['updated_by_open_name']);
}
}
@@ -0,0 +1,79 @@
<?php
namespace Tests\Unit\Services\Incidents;
use App\Services\Incidents\IncidentLookupService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class IncidentLookupServiceTest extends TestCase
{
use RefreshDatabase;
public function test_grade_options_with_active_students(): void
{
DB::table('classSection')->insert([
'id' => 1,
'class_id' => 1,
'class_section_id' => 10,
'class_section_name' => '1A',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('students')->insert([
'id' => 1,
'school_id' => 'S1',
'firstname' => 'Test',
'lastname' => 'Student',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
DB::table('student_class')->insert([
'student_id' => 1,
'class_section_id' => 10,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$service = new IncidentLookupService();
$grades = $service->gradeOptionsWithActiveStudents('2025-2026');
$this->assertSame([['id' => 10, 'name' => '1A']], $grades);
}
public function test_updater_name_map(): void
{
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Jane',
'lastname' => 'Doe',
'cellphone' => '5555555555',
'email' => 'jane@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 IncidentLookupService();
$map = $service->updaterNameMap([1]);
$this->assertSame('Jane Doe', $map[1]);
}
}
@@ -0,0 +1,38 @@
<?php
namespace Tests\Unit\Services\Invoices;
use App\Services\Invoices\InvoiceConfigService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class InvoiceConfigServiceTest extends TestCase
{
use RefreshDatabase;
public function test_config_service_reads_values(): void
{
DB::table('configuration')->insert([
['config_key' => 'school_year', 'config_value' => '2025-2026'],
['config_key' => 'semester', 'config_value' => 'Fall'],
['config_key' => 'due_date', 'config_value' => '2025-09-01'],
['config_key' => 'grade_fee', 'config_value' => '9'],
['config_key' => 'first_student_fee', 'config_value' => '350'],
['config_key' => 'second_student_fee', 'config_value' => '200'],
['config_key' => 'youth_fee', 'config_value' => '180'],
['config_key' => 'refund_deadline', 'config_value' => '2025-10-01'],
]);
$service = new InvoiceConfigService();
$this->assertSame('2025-2026', $service->getSchoolYear());
$this->assertSame('Fall', $service->getSemester());
$this->assertSame('2025-09-01', $service->getDueDate());
$this->assertSame(9, $service->getGradeFee());
$this->assertSame(350.0, $service->getFirstStudentFee());
$this->assertSame(200.0, $service->getSecondStudentFee());
$this->assertSame(180.0, $service->getYouthFee());
$this->assertSame('2025-10-01', $service->getRefundDeadline());
}
}
@@ -0,0 +1,92 @@
<?php
namespace Tests\Unit\Services\Invoices;
use App\Services\Invoices\InvoiceConfigService;
use App\Services\Invoices\InvoiceGenerationService;
use App\Services\Invoices\InvoiceGradeService;
use App\Services\Invoices\InvoiceTuitionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class InvoiceGenerationServiceTest extends TestCase
{
use RefreshDatabase;
public function test_generate_invoice_creates_new_invoice(): void
{
DB::table('users')->insert([
'id' => 10,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '5555555555',
'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' => 100,
'parent_id' => 10,
'firstname' => 'Kid',
'lastname' => 'User',
'school_id' => 1,
'is_active' => 1,
]);
DB::table('classSection')->insert([
'class_section_id' => 1,
'class_section_name' => '1A',
'class_id' => 1,
]);
DB::table('student_class')->insert([
'student_id' => 100,
'class_section_id' => 1,
'school_year' => '2025-2026',
'is_event_only' => 0,
]);
DB::table('enrollments')->insert([
'student_id' => 100,
'parent_id' => 10,
'class_section_id' => 1,
'enrollment_status' => 'enrolled',
'school_year' => '2025-2026',
]);
$config = new InvoiceConfigService();
$grades = new InvoiceGradeService($config->getGradeFee());
$tuition = new InvoiceTuitionService(
$grades,
$config->getGradeFee(),
$config->getFirstStudentFee(),
$config->getSecondStudentFee(),
$config->getYouthFee(),
$config->getTimezone()
);
$service = new InvoiceGenerationService($config, $tuition);
$result = $service->generateInvoice(10, '2025-2026');
$this->assertTrue($result['ok']);
$this->assertNotNull($result['insert_id']);
$this->assertDatabaseHas('invoices', [
'id' => $result['insert_id'],
'parent_id' => 10,
'school_year' => '2025-2026',
]);
}
}
@@ -0,0 +1,37 @@
<?php
namespace Tests\Unit\Services\Invoices;
use App\Services\Invoices\InvoiceGradeService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class InvoiceGradeServiceTest extends TestCase
{
use RefreshDatabase;
public function test_grade_parsing_handles_common_cases(): void
{
$service = new InvoiceGradeService(9);
$this->assertTrue($service->isKindergarten('KG'));
$this->assertSame(1, $service->gradeLevelInt('K'));
$this->assertSame(-1, $service->gradeLevelInt('PK'));
$this->assertSame(10, $service->gradeLevelInt('Youth'));
$this->assertSame(5, $service->gradeLevelInt('Grade 5'));
$info = $service->getGradeLevel('5A');
$this->assertSame(5, $info['level']);
$this->assertSame('A', $info['suffix']);
}
public function test_compare_grades_orders_numeric_then_suffix(): void
{
$service = new InvoiceGradeService(9);
$this->assertSame(-1, $service->compareGrades('1', '2'));
$this->assertSame(1, $service->compareGrades('2', '1'));
$this->assertSame(-1, $service->compareGrades('2A', '2B'));
$this->assertSame(0, $service->compareGrades('K', 'K'));
}
}
@@ -0,0 +1,107 @@
<?php
namespace Tests\Unit\Services\Invoices;
use App\Services\Invoices\InvoiceConfigService;
use App\Services\Invoices\InvoiceManagementService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class InvoiceManagementServiceTest extends TestCase
{
use RefreshDatabase;
public function test_management_data_returns_parent_invoice_summary(): 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' => '5555555555',
'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,
]);
DB::table('students')->insert([
'id' => 100,
'parent_id' => 10,
'firstname' => 'Kid',
'lastname' => 'User',
'school_id' => 1,
'is_active' => 1,
]);
DB::table('classSection')->insert([
'class_section_id' => 1,
'class_section_name' => '1A',
'class_id' => 1,
]);
DB::table('student_class')->insert([
'student_id' => 100,
'class_section_id' => 1,
'school_year' => '2025-2026',
'is_event_only' => 0,
]);
DB::table('enrollments')->insert([
'student_id' => 100,
'parent_id' => 10,
'class_section_id' => 1,
'enrollment_status' => 'enrolled',
'school_year' => '2025-2026',
]);
DB::table('invoices')->insert([
'id' => 1,
'parent_id' => 10,
'invoice_number' => 'INV-001',
'total_amount' => 100,
'balance' => 100,
'paid_amount' => 0,
'has_discount' => 0,
'issue_date' => '2025-01-10',
'status' => 'unpaid',
'school_year' => '2025-2026',
]);
DB::table('refunds')->insert([
'id' => 1,
'parent_id' => 10,
'school_year' => '2025-2026',
'invoice_id' => 1,
'refund_amount' => 5,
'status' => 'Paid',
'refund_paid_amount' => 5,
]);
$service = new InvoiceManagementService(new InvoiceConfigService());
$data = $service->getManagementData('2025-2026');
$this->assertSame('2025-2026', $data['schoolYear']);
$this->assertCount(1, $data['invoices']);
$this->assertSame(5.0, $data['invoices'][0]['refund_amount']);
}
}
@@ -0,0 +1,63 @@
<?php
namespace Tests\Unit\Services\Invoices;
use App\Services\Invoices\InvoiceConfigService;
use App\Services\Invoices\InvoicePaymentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class InvoicePaymentServiceTest extends TestCase
{
use RefreshDatabase;
public function test_parent_invoice_summary_includes_refunds_and_last_payment(): void
{
DB::table('invoices')->insert([
[
'id' => 1,
'parent_id' => 10,
'invoice_number' => 'INV-001',
'total_amount' => 100,
'balance' => 40,
'paid_amount' => 60,
'has_discount' => 0,
'issue_date' => '2025-01-10',
'status' => 'unpaid',
'school_year' => '2025-2026',
],
]);
DB::table('payments')->insert([
'id' => 1,
'parent_id' => 10,
'invoice_id' => 1,
'total_amount' => 100,
'paid_amount' => 60,
'balance' => 40,
'number_of_installments' => 1,
'payment_method' => 'Cash',
'payment_date' => '2025-01-15',
'school_year' => '2025-2026',
'status' => 'Paid',
]);
DB::table('refunds')->insert([
'id' => 1,
'parent_id' => 10,
'school_year' => '2025-2026',
'invoice_id' => 1,
'refund_amount' => 5,
'status' => 'Paid',
'refund_paid_amount' => 5,
]);
$service = new InvoicePaymentService(new InvoiceConfigService());
$data = $service->getParentInvoiceSummary(10, '2025-2026');
$this->assertSame('2025-2026', $data['selectedYear']);
$this->assertSame(5.0, $data['invoices'][0]['refund_amount']);
$this->assertSame(60.0, $data['invoices'][0]['last_paid_amount']);
}
}
@@ -0,0 +1,91 @@
<?php
namespace Tests\Unit\Services\Invoices;
use App\Services\Invoices\InvoiceConfigService;
use App\Services\Invoices\InvoiceGradeService;
use App\Services\Invoices\InvoicePdfService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class InvoicePdfServiceTest extends TestCase
{
use RefreshDatabase;
public function test_build_pdf_returns_bytes(): void
{
DB::table('users')->insert([
'id' => 10,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '5555555555',
'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' => 100,
'parent_id' => 10,
'firstname' => 'Kid',
'lastname' => 'User',
'school_id' => 1,
'is_active' => 1,
]);
DB::table('classSection')->insert([
'class_section_id' => 1,
'class_section_name' => '1A',
'class_id' => 1,
]);
DB::table('student_class')->insert([
'student_id' => 100,
'class_section_id' => 1,
'school_year' => '2025-2026',
'is_event_only' => 0,
]);
DB::table('enrollments')->insert([
'student_id' => 100,
'parent_id' => 10,
'class_section_id' => 1,
'enrollment_status' => 'enrolled',
'school_year' => '2025-2026',
]);
DB::table('invoices')->insert([
'id' => 1,
'parent_id' => 10,
'invoice_number' => 'INV-001',
'total_amount' => 100,
'balance' => 100,
'paid_amount' => 0,
'has_discount' => 0,
'issue_date' => '2025-01-10',
'status' => 'unpaid',
'school_year' => '2025-2026',
]);
$config = new InvoiceConfigService();
$grades = new InvoiceGradeService($config->getGradeFee());
$service = new InvoicePdfService($config, $grades);
$pdf = $service->buildPdf(1);
$this->assertIsString($pdf);
$this->assertNotSame('', $pdf);
}
}
@@ -0,0 +1,60 @@
<?php
namespace Tests\Unit\Services\Invoices;
use App\Services\Invoices\InvoiceGradeService;
use App\Services\Invoices\InvoiceTuitionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class InvoiceTuitionServiceTest extends TestCase
{
use RefreshDatabase;
public function test_calculate_tuition_fee_counts_regular_and_youth(): void
{
DB::table('classSection')->insert([
['class_section_id' => 1, 'class_section_name' => '1', 'class_id' => 1],
['class_section_id' => 2, 'class_section_name' => '2', 'class_id' => 2],
['class_section_id' => 3, 'class_section_name' => '10', 'class_id' => 10],
]);
$grades = new InvoiceGradeService(9);
$service = new InvoiceTuitionService($grades, 9, 350.0, 200.0, 180.0, 'UTC');
$registeredKids = [
['class_section_id' => 1],
['class_section_id' => 2],
['class_section_id' => 3],
];
$total = $service->calculateTuitionFee($registeredKids, [], date('Y-m-d', strtotime('+10 days')));
$this->assertSame(730.0, $total);
}
public function test_refund_deadline_includes_withdrawn_when_passed(): void
{
DB::table('classSection')->insert([
['class_section_id' => 1, 'class_section_name' => '1', 'class_id' => 1],
['class_section_id' => 2, 'class_section_name' => '2', 'class_id' => 2],
]);
$grades = new InvoiceGradeService(9);
$service = new InvoiceTuitionService($grades, 9, 350.0, 200.0, 180.0, 'UTC');
$registeredKids = [
['class_section_id' => 1],
];
$withdrawnKids = [
['class_section_id' => 2],
];
$totalAllowed = $service->calculateTuitionFee($registeredKids, $withdrawnKids, date('Y-m-d', strtotime('+10 days')));
$totalBlocked = $service->calculateTuitionFee($registeredKids, $withdrawnKids, date('Y-m-d', strtotime('-10 days')));
$this->assertSame(350.0, $totalAllowed);
$this->assertSame(550.0, $totalBlocked);
}
}
@@ -0,0 +1,40 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\Payments\PaymentBalanceService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class PaymentBalanceServiceTest extends TestCase
{
use RefreshDatabase;
public function test_update_balance_adjusts_payment_totals(): void
{
DB::table('payments')->insert([
'id' => 1,
'parent_id' => 10,
'invoice_id' => 1,
'total_amount' => 100,
'paid_amount' => 0,
'balance' => 100,
'number_of_installments' => 1,
'payment_method' => 'cash',
'payment_date' => '2025-01-01',
'school_year' => '2025-2026',
'status' => 'Pending',
]);
$service = new PaymentBalanceService();
$updated = $service->updateBalance(1, 40);
$this->assertTrue($updated);
$this->assertDatabaseHas('payments', [
'id' => 1,
'paid_amount' => 40,
'balance' => 60,
]);
}
}
@@ -0,0 +1,53 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\Payments\PaymentEnrollmentEventService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class PaymentEnrollmentEventServiceTest extends TestCase
{
use RefreshDatabase;
public function test_build_event_data_requires_parent(): void
{
$service = new PaymentEnrollmentEventService();
$this->expectException(\RuntimeException::class);
$service->buildEventData(999, [], '2025-2026', 'Fall');
}
public function test_build_event_data_returns_parent_payload_when_no_students(): void
{
DB::table('users')->insert([
'id' => 10,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '555-555-5555',
'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 PaymentEnrollmentEventService();
[$parent, $students] = $service->buildEventData(10, [], '2025-2026', 'Fall', 'https://example.test/login');
$this->assertSame(10, $parent['user_id']);
$this->assertSame('parent@example.com', $parent['email']);
$this->assertSame([], $students);
}
}
@@ -0,0 +1,82 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\Payments\PaymentEventChargesService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class PaymentEventChargesServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_enrolled_students_returns_grade(): void
{
DB::table('students')->insert([
'id' => 1,
'school_id' => 'S1',
'firstname' => 'Student',
'lastname' => 'One',
'dob' => '2010-01-01',
'age' => 14,
'gender' => 'Male',
'is_active' => 1,
'registration_grade' => '5',
'is_new' => 1,
'photo_consent' => 1,
'parent_id' => 10,
'registration_date' => '2025-01-01',
'tuition_paid' => 0,
'rfid_tag' => null,
'semester' => 'Fall',
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
DB::table('enrollments')->insert([
'id' => 1,
'student_id' => 1,
'class_section_id' => 100,
'parent_id' => 10,
'enrollment_date' => '2025-09-01',
'enrollment_status' => 'enrolled',
'withdrawal_date' => null,
'is_withdrawn' => 0,
'admission_status' => 'accepted',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('classSection')->insert([
'id' => 1,
'class_id' => 9,
'class_section_id' => 100,
'class_section_name' => 'Section A',
'created_at' => null,
'updated_at' => null,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
DB::table('student_class')->insert([
'id' => 1,
'student_id' => 1,
'class_section_id' => 100,
'is_event_only' => 0,
'semester' => 'Fall',
'school_year' => '2025-2026',
'description' => null,
'created_at' => '2025-09-01 00:00:00',
'updated_at' => '2025-09-01 00:00:00',
'updated_by' => null,
]);
$service = new PaymentEventChargesService();
$students = $service->getEnrolledStudents(10, '2025-2026');
$this->assertCount(1, $students);
$this->assertSame(1, $students[0]['id']);
$this->assertSame('9', (string) $students[0]['grade']);
}
}
@@ -0,0 +1,50 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\Payments\PaymentLookupService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class PaymentLookupServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_by_parent_filters_school_year(): void
{
DB::table('payments')->insert([
'id' => 1,
'parent_id' => 10,
'invoice_id' => 1,
'total_amount' => 100,
'paid_amount' => 20,
'balance' => 80,
'number_of_installments' => 1,
'payment_method' => 'cash',
'payment_date' => '2025-01-01',
'school_year' => '2025-2026',
'status' => 'Pending',
]);
DB::table('payments')->insert([
'id' => 2,
'parent_id' => 10,
'invoice_id' => 2,
'total_amount' => 100,
'paid_amount' => 20,
'balance' => 80,
'number_of_installments' => 1,
'payment_method' => 'cash',
'payment_date' => '2024-01-01',
'school_year' => '2024-2025',
'status' => 'Pending',
]);
$service = new PaymentLookupService();
$results = $service->getByParent(10, '2025-2026');
$this->assertCount(1, $results);
$this->assertSame(1, $results[0]['id']);
}
}
@@ -0,0 +1,61 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\Discounts\DiscountInvoiceService;
use App\Services\Payments\PaymentEnrollmentEventService;
use App\Services\Payments\PaymentManualService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class PaymentManualServiceTest extends TestCase
{
use RefreshDatabase;
public function test_record_payment_rejects_invalid_method(): void
{
$invoiceService = Mockery::mock(DiscountInvoiceService::class);
$enrollmentService = Mockery::mock(PaymentEnrollmentEventService::class);
$service = new PaymentManualService($invoiceService, $enrollmentService);
$this->expectException(\InvalidArgumentException::class);
$service->recordPayment(1, 10.0, 'wire', null, null, null, null);
}
public function test_suggest_returns_matches(): void
{
DB::table('users')->insert([
'id' => 10,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '555-555-5555',
'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',
]);
$invoiceService = Mockery::mock(DiscountInvoiceService::class);
$enrollmentService = Mockery::mock(PaymentEnrollmentEventService::class);
$service = new PaymentManualService($invoiceService, $enrollmentService);
$results = $service->suggest('parent@example.com');
$this->assertCount(1, $results);
$this->assertSame('parent@example.com', $results[0]['value']);
}
}
@@ -0,0 +1,40 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\Payments\PaymentNotificationDispatchService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class PaymentNotificationDispatchServiceTest extends TestCase
{
use RefreshDatabase;
public function test_notify_user_creates_notification_records(): void
{
DB::table('configuration')->insert([
['id' => 2001, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
['id' => 2002, 'config_key' => 'semester', 'config_value' => 'Fall'],
]);
$service = new PaymentNotificationDispatchService();
$service->notifyUser(10, 'Payment Reminder', 'Reminder body', ['in_app']);
$this->assertDatabaseHas('notifications', [
'title' => 'Payment Reminder',
'message' => 'Reminder body',
'target_group' => 'user',
'status' => 'sent',
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
$this->assertDatabaseHas('user_notifications', [
'user_id' => 10,
'delivered' => 1,
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
}
}
@@ -0,0 +1,134 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\EmailService;
use App\Services\Payments\PaymentNotificationDispatchService;
use App\Services\Payments\PaymentNotificationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class PaymentNotificationServiceTest extends TestCase
{
use RefreshDatabase;
public function test_send_records_log_and_returns_sent_count(): void
{
DB::table('users')->insert([
'id' => 10,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '555-555-5555',
'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('invoices')->insert([
'id' => 1,
'parent_id' => 10,
'invoice_number' => 'INV-1',
'total_amount' => 100,
'balance' => 100,
'paid_amount' => 0,
'has_discount' => 0,
'issue_date' => '2025-01-01',
'status' => 'unpaid',
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
$emailService = Mockery::mock(EmailService::class);
$emailService->shouldReceive('send')
->once()
->with(
'parent@example.com',
Mockery::type('string'),
Mockery::type('string'),
'finance'
)
->andReturn(true);
$dispatchService = Mockery::mock(PaymentNotificationDispatchService::class);
$dispatchService->shouldNotReceive('notifyUser');
$service = new PaymentNotificationService($emailService, $dispatchService);
$result = $service->send([
'parent_id' => 10,
'type' => 'installment',
'school_year' => '2025-2026',
]);
$this->assertSame(1, $result['sent']);
$this->assertDatabaseHas('payment_notification_logs', [
'parent_id' => 10,
'status' => 'sent',
'type' => 'installment',
]);
}
public function test_list_logs_filters_by_type(): void
{
DB::table('users')->insert([
'id' => 11,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'Two',
'cellphone' => '555-555-5556',
'email' => 'parent2@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('payment_notification_logs')->insert([
'id' => 1,
'parent_id' => 11,
'invoice_id' => null,
'school_year' => '2025-2026',
'period_year' => 2025,
'period_month' => 9,
'type' => 'installment',
'to_email' => 'parent2@example.com',
'cc_email' => null,
'head_fa_notified' => 0,
'subject' => 'Reminder',
'body' => 'Body',
'status' => 'sent',
'error_message' => null,
'balance_snapshot' => 50,
'sent_at' => '2025-09-01 00:00:00',
]);
$emailService = Mockery::mock(EmailService::class);
$dispatchService = Mockery::mock(PaymentNotificationDispatchService::class);
$service = new PaymentNotificationService($emailService, $dispatchService);
$logs = $service->listLogs(null, null, 'installment');
$this->assertCount(1, $logs);
$this->assertSame(11, $logs[0]->parent_id);
}
}
@@ -0,0 +1,70 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\Payments\PaymentPlanService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class PaymentPlanServiceTest extends TestCase
{
use RefreshDatabase;
public function test_create_plan_uses_defaults_when_missing(): void
{
DB::table('configuration')->insert([
['id' => 2001, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
['id' => 2002, 'config_key' => 'semester', 'config_value' => 'Fall'],
]);
DB::table('users')->insert([
'id' => 10,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '5555555555',
'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('invoices')->insert([
'id' => 1,
'parent_id' => 10,
'invoice_number' => 'INV-1',
'total_amount' => 200,
'balance' => 200,
'paid_amount' => 0,
'has_discount' => 0,
'issue_date' => '2025-01-01',
'status' => 'unpaid',
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
$service = new PaymentPlanService();
$payment = $service->createPlan([
'parent_id' => 10,
'invoice_id' => 1,
'total_amount' => 200,
'number_of_installments' => 2,
'payment_date' => '2025-01-01',
'payment_method' => 'cash',
]);
$this->assertSame('2025-2026', $payment->school_year);
$this->assertSame('Fall', $payment->semester);
$this->assertSame(200.0, (float) $payment->total_amount);
}
}
@@ -0,0 +1,67 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\Payments\PaymentTransactionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class PaymentTransactionServiceTest extends TestCase
{
use RefreshDatabase;
public function test_create_and_update_transaction(): void
{
if (!Schema::hasColumn('payment_transactions', 'payment_reference')) {
Schema::table('payment_transactions', function (Blueprint $table) {
$table->string('payment_reference')->nullable();
});
}
if (!Schema::hasColumn('payment_transactions', 'is_full_payment')) {
Schema::table('payment_transactions', function (Blueprint $table) {
$table->boolean('is_full_payment')->default(0);
});
}
DB::table('payments')->insert([
'id' => 1,
'parent_id' => 10,
'invoice_id' => 1,
'total_amount' => 100,
'paid_amount' => 0,
'balance' => 100,
'number_of_installments' => 1,
'payment_method' => 'card',
'payment_date' => '2025-01-01',
'school_year' => '2025-2026',
'status' => 'Pending',
]);
$service = new PaymentTransactionService();
$transaction = $service->create([
'transaction_id' => 'TXN-1',
'payment_id' => 1,
'transaction_date' => '2025-01-02 00:00:00',
'amount' => 50,
'payment_method' => 'card',
'payment_status' => 'Pending',
'transaction_fee' => 1.25,
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
$this->assertSame('TXN-1', $transaction->transaction_id);
$updated = $service->updateStatus('TXN-1', 'Completed');
$this->assertTrue($updated);
$this->assertDatabaseHas('payment_transactions', [
'transaction_id' => 'TXN-1',
'payment_status' => 'Completed',
]);
}
}
@@ -0,0 +1,45 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\Payments\PaypalPaymentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class PaypalPaymentServiceTest extends TestCase
{
use RefreshDatabase;
public function test_create_payment_falls_back_to_hosted_when_sdk_missing(): void
{
DB::table('payments')->insert([
'id' => 1,
'parent_id' => 10,
'invoice_id' => 1,
'total_amount' => 100,
'paid_amount' => 0,
'balance' => 100,
'number_of_installments' => 1,
'payment_method' => 'card',
'payment_date' => '2025-01-01',
'school_year' => '2025-2026',
'status' => 'Pending',
]);
$service = new PaypalPaymentService();
$result = $service->createPayment(1);
$this->assertSame('hosted', $result['mode']);
$this->assertSame($service->getRedirectUrl(), $result['redirect_url']);
}
public function test_execute_payment_throws_when_sdk_missing(): void
{
$service = new PaypalPaymentService();
$this->expectException(\RuntimeException::class);
$service->executePayment('payer');
}
}
@@ -0,0 +1,46 @@
<?php
namespace Tests\Unit\Services\Payments;
use App\Services\Payments\PaypalTransactionsService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class PaypalTransactionsServiceTest extends TestCase
{
use RefreshDatabase;
public function test_list_all_filters_by_keyword(): void
{
DB::table('paypal_payments')->insert([
'id' => 1,
'transaction_id' => 'TXN-ABC',
'payer_email' => 'payer@example.com',
'event_type' => 'PAYMENT',
'order_id' => 'ORDER-1',
'parent_school_id' => 'P1',
'amount' => 50,
'currency' => 'USD',
'created_at' => '2025-01-01 00:00:00',
]);
DB::table('paypal_payments')->insert([
'id' => 2,
'transaction_id' => 'TXN-DEF',
'payer_email' => 'other@example.com',
'event_type' => 'REFUND',
'order_id' => 'ORDER-2',
'parent_school_id' => 'P2',
'amount' => 75,
'currency' => 'USD',
'created_at' => '2025-01-02 00:00:00',
]);
$service = new PaypalTransactionsService();
$results = $service->listAll('ABC');
$this->assertCount(1, $results);
$this->assertStringContainsString('TXN-ABC', json_encode($results));
}
}
@@ -0,0 +1,83 @@
<?php
namespace Tests\Unit\Services\Reimbursements;
use App\Services\Reimbursements\ReimbursementBatchAssignmentService;
use App\Services\Reimbursements\ReimbursementLookupService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ReimbursementBatchAssignmentServiceTest extends TestCase
{
use RefreshDatabase;
public function test_assign_and_unassign_batch_item(): void
{
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Admin',
'lastname' => 'User',
'cellphone' => '5555555555',
'email' => 'admin@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('reimbursement_batches')->insert([
'id' => 10,
'title' => 'Batch 10',
'status' => 'open',
'school_year' => '2025-2026',
'semester' => 'Fall',
'opened_at' => now(),
]);
DB::table('expenses')->insert([
'id' => 100,
'category' => 'Expense',
'amount' => 25.00,
'date_of_purchase' => '2025-01-01',
'added_by' => 1,
'purchased_by' => 1,
'status' => 'approved',
]);
$service = new ReimbursementBatchAssignmentService(new ReimbursementLookupService());
$result = $service->updateAssignment(100, 10, 1, null, '2025-2026', 'Fall');
$this->assertSame(10, $result['batch_id']);
$this->assertDatabaseHas('reimbursement_batch_items', [
'batch_id' => 10,
'expense_id' => 100,
'admin_id' => 1,
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
$service->updateAssignment(100, 0, null, null, '2025-2026', 'Fall');
$this->assertDatabaseHas('reimbursement_batch_items', [
'batch_id' => 10,
'expense_id' => 100,
]);
$this->assertDatabaseMissing('reimbursement_batch_items', [
'batch_id' => 10,
'expense_id' => 100,
'unassigned_at' => null,
]);
}
}
@@ -0,0 +1,30 @@
<?php
namespace Tests\Unit\Services\Reimbursements;
use App\Services\Reimbursements\ReimbursementBatchService;
use App\Services\Reimbursements\ReimbursementLookupService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ReimbursementBatchServiceTest extends TestCase
{
use RefreshDatabase;
public function test_create_batch_assigns_sequence_and_title(): void
{
$service = new ReimbursementBatchService(new ReimbursementLookupService());
$batch = $service->createBatch(null, 1, '2025-2026', 'Fall');
$this->assertSame(1, $batch['sequence']);
$this->assertStringContainsString('Batch', $batch['label']);
$this->assertDatabaseHas('reimbursement_batches', [
'id' => $batch['batch_id'],
'status' => 'open',
'school_year' => '2025-2026',
'semester' => 'Fall',
]);
}
}
@@ -0,0 +1,77 @@
<?php
namespace Tests\Unit\Services\Reimbursements;
use App\Services\Reimbursements\ReimbursementDonationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ReimbursementDonationServiceTest extends TestCase
{
use RefreshDatabase;
public function test_mark_donation_updates_expense_and_unassigns(): void
{
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Admin',
'lastname' => 'User',
'cellphone' => '5555555555',
'email' => 'admin@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('expenses')->insert([
'id' => 200,
'category' => 'Expense',
'amount' => 40.00,
'date_of_purchase' => '2025-02-01',
'added_by' => 1,
'purchased_by' => 1,
'status' => 'approved',
]);
DB::table('reimbursement_batches')->insert([
'id' => 5,
'title' => 'Batch 5',
'status' => 'open',
'school_year' => '2025-2026',
'semester' => 'Fall',
'opened_at' => now(),
]);
DB::table('reimbursement_batch_items')->insert([
'batch_id' => 5,
'expense_id' => 200,
'assigned_at' => now(),
]);
$service = new ReimbursementDonationService();
$service->markDonation(200, 1);
$this->assertDatabaseHas('expenses', [
'id' => 200,
'category' => 'Donation',
'status' => 'approved',
]);
$this->assertDatabaseMissing('reimbursement_batch_items', [
'batch_id' => 5,
'expense_id' => 200,
'unassigned_at' => null,
]);
}
}
@@ -0,0 +1,79 @@
<?php
namespace Tests\Unit\Services\Reimbursements;
use App\Services\Reimbursements\ReimbursementRecipientService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ReimbursementRecipientServiceTest extends TestCase
{
use RefreshDatabase;
public function test_recipient_options_include_staff_and_specials(): void
{
DB::table('users')->insert([
[
'id' => 1,
'school_id' => 1,
'firstname' => 'Admin',
'lastname' => 'User',
'cellphone' => '5555555555',
'email' => 'admin@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',
],
[
'id' => 2,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '5555555555',
'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('roles')->insert([
['id' => 1, 'name' => 'admin'],
['id' => 2, 'name' => 'parent'],
]);
DB::table('user_roles')->insert([
['user_id' => 1, 'role_id' => 1],
['user_id' => 2, 'role_id' => 2],
]);
$service = new ReimbursementRecipientService();
$recipients = $service->recipientOptions();
$ids = array_map(static fn ($row) => (int) ($row['id'] ?? 0), $recipients);
$this->assertContains(1, $ids);
$this->assertNotContains(2, $ids);
$this->assertContains(990001, $ids);
$this->assertContains(990002, $ids);
}
}
@@ -0,0 +1,106 @@
<?php
namespace Tests\Unit\Services\Scores;
use App\Services\Scores\ExamScoreService;
use App\Services\Scores\ScoreTermService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ExamScoreServiceTest extends TestCase
{
use RefreshDatabase;
public function test_list_returns_latest_exam_score(): void
{
$this->seedStudent(1, 'SCH1', 'Leo', 'Park');
$this->seedStudentClass(1, 3, '2025-2026');
DB::table('midterm_exam')->insert([
[
'student_id' => 1,
'school_id' => 'SCH1',
'class_section_id' => 3,
'updated_by' => 99,
'score' => 70,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
[
'student_id' => 1,
'school_id' => 'SCH1',
'class_section_id' => 3,
'updated_by' => 99,
'score' => 80,
'semester' => 'Fall',
'school_year' => '2025-2026',
],
]);
$service = new ExamScoreService(new ScoreTermService());
$result = $service->list('midterm_exam', 3, 'Fall', '2025-2026');
$this->assertCount(1, $result['students']);
$this->assertSame(80.0, $result['students'][0]['score']);
}
public function test_update_creates_exam_score_and_missing_override(): void
{
$this->seedStudent(1, 'SCH1', 'Leo', 'Park');
$this->seedStudentClass(1, 3, '2025-2026');
$service = new ExamScoreService(new ScoreTermService());
$count = $service->update(
'midterm_exam',
3,
'Fall',
'2025-2026',
[1 => ['score' => 95]],
[1 => true],
99
);
$this->assertSame(1, $count);
$this->assertDatabaseHas('midterm_exam', [
'student_id' => 1,
'class_section_id' => 3,
'score' => 95.0,
]);
$this->assertDatabaseHas('missing_score_overrides', [
'student_id' => 1,
'class_section_id' => 3,
'item_type' => 'midterm_exam',
'item_index' => null,
'is_allowed' => 1,
]);
}
private function seedStudent(int $id, string $schoolId, string $first, string $last): void
{
DB::table('students')->insert([
'id' => $id,
'school_id' => $schoolId,
'firstname' => $first,
'lastname' => $last,
'age' => 11,
'gender' => 'M',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'is_active' => 1,
]);
}
private function seedStudentClass(int $studentId, int $classSectionId, string $schoolYear): void
{
DB::table('student_class')->insert([
'student_id' => $studentId,
'class_section_id' => $classSectionId,
'school_year' => $schoolYear,
'semester' => 'Fall',
'is_event_only' => 0,
]);
}
}
@@ -0,0 +1,81 @@
<?php
namespace Tests\Unit\Services\Scores;
use App\Services\Scores\HomeworkScoreService;
use App\Services\Scores\ScoreTermService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class HomeworkScoreServiceTest extends TestCase
{
use RefreshDatabase;
public function test_list_includes_semester_variants(): void
{
$this->seedStudent(1, 'SCH1', 'Eli', 'Page');
$this->seedStudentClass(1, 8, '2025-2026');
DB::table('homework')->insert([
'student_id' => 1,
'school_id' => 'SCH1',
'class_section_id' => 8,
'updated_by' => 99,
'homework_index' => 1,
'score' => 91,
'semester' => 'First Semester',
'school_year' => '2025-2026',
]);
$service = new HomeworkScoreService(new ScoreTermService());
$result = $service->list(8, 'Fall', '2025-2026');
$this->assertSame([1], $result['headers']);
$this->assertSame(91.0, $result['students'][0]['scores'][1]);
}
public function test_add_column_inserts_next_index(): void
{
$this->seedStudent(1, 'SCH1', 'Eli', 'Page');
$this->seedStudentClass(1, 8, '2025-2026');
$service = new HomeworkScoreService(new ScoreTermService());
$nextIndex = $service->addColumn(8, 'Fall', '2025-2026', 99);
$this->assertSame(1, $nextIndex);
$this->assertDatabaseHas('homework', [
'student_id' => 1,
'class_section_id' => 8,
'homework_index' => 1,
]);
}
private function seedStudent(int $id, string $schoolId, string $first, string $last): void
{
DB::table('students')->insert([
'id' => $id,
'school_id' => $schoolId,
'firstname' => $first,
'lastname' => $last,
'age' => 10,
'gender' => 'M',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'is_active' => 1,
]);
}
private function seedStudentClass(int $studentId, int $classSectionId, string $schoolYear): void
{
DB::table('student_class')->insert([
'student_id' => $studentId,
'class_section_id' => $classSectionId,
'school_year' => $schoolYear,
'semester' => 'Fall',
'is_event_only' => 0,
]);
}
}

Some files were not shown because too many files have changed in this diff Show More