add controllers, servoices
This commit is contained in:
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user