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