reconstruction of the project

This commit is contained in:
root
2026-03-08 16:33:24 -04:00
parent 23b7db1107
commit c8de5f7edc
9157 changed files with 77877 additions and 1073823 deletions
@@ -0,0 +1,23 @@
<?php
namespace Tests\Feature\Api\Attendance;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class AdminAttendanceApiControllerTest extends TestCase
{
use RefreshDatabase;
public function test_daily_data_returns_success(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->getJson('/api/attendance/admin/daily');
$response->assertStatus(200);
}
}
@@ -0,0 +1,57 @@
<?php
namespace Tests\Unit\Services\Attendance;
use App\Services\Attendance\AttendancePolicyService;
use PHPUnit\Framework\TestCase;
class AttendancePolicyServiceTest extends TestCase
{
public function test_teacher_can_edit_draft_day(): void
{
$service = new AttendancePolicyService();
$day = ['status' => 'draft'];
$user = [
'roles' => ['teacher'],
'permissions' => [],
];
$this->assertTrue($service->canEditDay($day, $user));
}
public function test_teacher_cannot_edit_finalized_day(): void
{
$service = new AttendancePolicyService();
$day = ['status' => 'finalized'];
$user = [
'roles' => ['teacher'],
'permissions' => [],
];
$this->assertFalse($service->canEditDay($day, $user));
}
public function test_admin_can_edit_finalized_day(): void
{
$service = new AttendancePolicyService();
$day = ['status' => 'finalized'];
$user = [
'roles' => ['admin'],
'permissions' => [],
];
$this->assertTrue($service->canEditDay($day, $user));
}
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,267 @@
<?php
namespace Tests\Unit\Services\Attendance;
use App\Models\AttendanceRecord;
use App\Services\Attendance\AttendanceRecordSyncService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AttendanceRecordSyncServiceTest extends TestCase
{
use RefreshDatabase;
protected AttendanceRecordSyncService $service;
protected function setUp(): 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',
'total_presence' => 5,
'total_absence' => 2,
'total_late' => 1,
'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',
]);
}
public function test_update_attendance_record_never_goes_below_zero(): 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
);
$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',
'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,
]);
}
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',
]);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
<?php
namespace Tests\Unit\Services\Attendance;
use App\Services\Attendance\SemesterRangeService;
use PHPUnit\Framework\TestCase;
class SemesterRangeServiceTest extends TestCase
{
public function test_normalize_semester(): void
{
$service = new SemesterRangeService();
$this->assertSame('Fall', $service->normalizeSemester('fall'));
$this->assertSame('Spring', $service->normalizeSemester('SPRING'));
$this->assertSame('', $service->normalizeSemester('summer'));
}
public function test_get_school_year_range(): void
{
$service = new SemesterRangeService();
[$start, $end] = $service->getSchoolYearRange('2025-2026');
$this->assertSame('2025-09-01', $start);
$this->assertSame('2026-05-31', $end);
}
public function test_build_sunday_list(): void
{
$service = new SemesterRangeService();
$sundays = $service->buildSundayList('2025-09-01', '2025-09-30');
$this->assertContains('2025-09-07', $sundays);
$this->assertContains('2025-09-14', $sundays);
$this->assertContains('2025-09-21', $sundays);
$this->assertContains('2025-09-28', $sundays);
}
}
@@ -0,0 +1,33 @@
<?php
namespace Tests\Feature\Api\Attendance;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class StaffAttendanceApiControllerTest extends TestCase
{
use RefreshDatabase;
public function test_month_data_returns_success(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->getJson('/api/attendance/staff/month');
$response->assertStatus(200);
}
public function test_admins_data_returns_success(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->getJson('/api/attendance/staff/admins');
$response->assertStatus(200);
}
}
@@ -0,0 +1,322 @@
<?php
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 Tests\TestCase;
class StudentAttendanceWriterServiceTest extends TestCase
{
use RefreshDatabase;
protected StudentAttendanceWriterService $service;
protected function setUp(): 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(),
]);
$result = $this->service->resolveStudentSchoolId(101, null);
$this->assertSame('SCH-101', $result);
}
public function test_resolve_student_school_id_throws_when_student_missing(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Student school ID not found.');
$this->service->resolveStudentSchoolId(9999, null);
}
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
);
$this->assertDatabaseHas('attendance_data', [
'class_id' => 5,
'class_section_id' => 10,
'student_id' => 200,
'school_id' => 'SCH-200',
'status' => 'absent',
'reason' => 'Sick',
'is_reported' => 'yes',
'is_notified' => 'no',
'semester' => 'Fall',
'school_year' => '2025-2026',
'date' => '2025-10-05',
'modified_by' => 22,
]);
}
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',
'status' => 'absent',
'reason' => 'Family trip',
'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);
}
}
@@ -0,0 +1,33 @@
<?php
namespace Tests\Feature\Api\Attendance;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class TeacherAttendanceApiControllerTest extends TestCase
{
use RefreshDatabase;
public function test_teacher_grid_endpoint_returns_success(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->getJson('/api/attendance/teacher/grid');
$response->assertStatus(200);
}
public function test_teacher_form_endpoint_returns_success_or_validation_message(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->getJson('/api/attendance/teacher/form');
$this->assertContains($response->status(), [200, 422]);
}
}
@@ -0,0 +1,545 @@
<?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']);
}
}