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
@@ -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']);
}
}
}