545 lines
18 KiB
PHP
545 lines
18 KiB
PHP
<?php
|
|
|
|
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
|
|
{
|
|
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']);
|
|
}
|
|
} |