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 { DB::table('classSection')->insert([ 'id' => 1, 'class_id' => 5, 'class_section_id' => 100, ]); $teacherClassMock = Mockery::mock(TeacherClass::class); $teacherClassMock->shouldReceive('assignedForSectionTerm') ->once() ->andReturn([ [ 'teacher_id' => 55, '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('No student attendance data provided.'); $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' ); } public function test_submit_throws_when_teacher_attendance_missing(): void { 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', '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, ]); } 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']); } }