add controllers, servoices

This commit is contained in:
root
2026-03-09 02:52:13 -04:00
parent c8de5f7edc
commit d76c871cb7
501 changed files with 34439 additions and 21843 deletions
@@ -2,44 +2,28 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceData;
use App\Models\AttendanceTracking;
use App\Models\Student;
use App\Services\AttendanceCaseQueryService;
use App\Services\AttendanceParentLookupService;
use App\Services\ViolationRuleEngineService;
use App\Services\AttendanceTracking\AttendanceCaseQueryService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class AttendanceCaseQueryServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_returns_not_found_when_student_missing(): void
{
Mockery::close();
parent::tearDown();
}
public function test_get_student_case_returns_404_when_student_not_found(): void
{
$student = Mockery::mock(Student::class);
$student->shouldReceive('query->find')->with(99)->andReturn(null);
$attendanceData = Mockery::mock(AttendanceData::class);
$tracking = Mockery::mock(AttendanceTracking::class);
$rules = Mockery::mock(ViolationRuleEngineService::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new AttendanceCaseQueryService(
$student,
$attendanceData,
$tracking,
$rules,
$parents
new \App\Models\Student(),
new \App\Models\AttendanceData(),
new \App\Models\AttendanceTracking(),
Mockery::mock(\App\Services\AttendanceTracking\ViolationRuleEngineService::class),
Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class)
);
$result = $service->getStudentCase(99);
$result = $service->getStudentCase(999);
$this->assertFalse($result['success']);
$this->assertSame(404, $result['status']);
}
}
}
@@ -2,72 +2,62 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceData;
use App\Models\Configuration;
use App\Models\Student;
use App\Services\AttendanceCommunicationSupportService;
use App\Services\AttendanceEmailComposerService;
use App\Services\AttendanceParentLookupService;
use App\Services\AttendanceTracking\AttendanceCommunicationSupportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class AttendanceCommunicationSupportServiceTest extends TestCase
{
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
use RefreshDatabase;
protected function makeService(
?Student $student = null,
?AttendanceData $attendanceData = null,
?Configuration $config = null,
?AttendanceParentLookupService $parents = null,
?AttendanceEmailComposerService $composer = null
): AttendanceCommunicationSupportService {
$student ??= Mockery::mock(Student::class);
$attendanceData ??= Mockery::mock(AttendanceData::class);
$config ??= Mockery::mock(Configuration::class);
$parents ??= Mockery::mock(AttendanceParentLookupService::class);
$composer ??= Mockery::mock(AttendanceEmailComposerService::class);
$config->shouldReceive('getConfig')->with('semester')->andReturn('Fall');
$config->shouldReceive('getConfig')->with('school_year')->andReturn('2025-2026');
return new AttendanceCommunicationSupportService(
$student,
$attendanceData,
$config,
$parents,
$composer
);
}
public function test_compose_returns_422_when_student_id_missing(): void
public function test_compose_requires_student_id(): void
{
$service = $this->makeService();
$result = $service->compose(0);
$this->assertFalse($result['success']);
$this->assertSame(422, $result['status']);
}
public function test_parents_info_delegates_to_parent_lookup_service(): void
public function test_get_violation_dates_for_student(): void
{
$parents = Mockery::mock(AttendanceParentLookupService::class);
$parents->shouldReceive('parentsInfo')
->once()
->with(20, '2025-2026')
->andReturn(['success' => true, 'data' => ['primary' => [], 'secondary' => []]]);
DB::table('configuration')->insert([
['config_key' => 'semester', 'config_value' => 'Fall'],
['config_key' => 'school_year', 'config_value' => '2025-2026'],
]);
$service = $this->makeService(
parents: $parents
);
DB::table('attendance_data')->insert([
'class_id' => 1,
'class_section_id' => 1,
'student_id' => 1,
'school_id' => 'S1',
'date' => '2025-01-01',
'status' => 'absent',
'is_reported' => 'no',
'is_notified' => 'no',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$result = $service->parentsInfo(20);
$service = $this->makeService();
$dates = $service->getViolationDatesForStudent(1, 'ABS_1');
$this->assertTrue($result['success']);
$this->assertSame(['2025-01-01'], $dates);
}
}
private function makeService(): AttendanceCommunicationSupportService
{
$parentLookup = Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class);
$emailComposer = Mockery::mock(\App\Services\AttendanceTracking\AttendanceEmailComposerService::class);
return new AttendanceCommunicationSupportService(
new \App\Models\Student(),
new \App\Models\AttendanceData(),
new \App\Models\Configuration(),
$parentLookup,
$emailComposer
);
}
}
@@ -2,124 +2,37 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceEmailTemplate;
use App\Services\AttendanceEmailComposerService;
use Mockery;
use App\Services\AttendanceTracking\AttendanceEmailComposerService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AttendanceEmailComposerServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_render_template_replaces_tokens(): void
{
Mockery::close();
parent::tearDown();
}
public function test_build_template_context_returns_expected_placeholders(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
$context = $service->buildTemplateContext(
[
'name' => 'John Doe',
'last_date' => '2026-03-01',
],
[
'parent_name' => 'Jane Doe',
'phone' => '555-1212',
]
);
$this->assertSame('Jane Doe', $context['{{parent_name}}']);
$this->assertSame('John Doe', $context['{{student_name}}']);
$this->assertSame('2026-03-01', $context['{{incident_date}}']);
$this->assertSame('555-1212', $context['{{voicemail_phone}}']);
}
public function test_render_template_uses_model_get_template(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$model->shouldReceive('getTemplate')
->once()
->with('ABS_1', 'default')
->andReturn([
'subject' => 'Notice for {{student_name}}',
'body_html' => '<p>Date: {{incident_date}}</p>',
]);
$service = new AttendanceEmailComposerService($model);
$rendered = $service->renderTemplate('ABS_1', 'default', [
'{{student_name}}' => 'John Doe',
'{{incident_date}}' => '2026-03-01',
DB::table('email_templates')->insert([
'code' => 'ABS_1',
'variant' => 'default',
'subject' => 'Notice for {{student_name}}',
'body_html' => '<p>{{parent_name}}</p>',
'is_active' => 1,
]);
$this->assertSame(
['Notice for John Doe', '<p>Date: 2026-03-01</p>'],
$rendered
);
$service = new AttendanceEmailComposerService(new \App\Models\AttendanceEmailTemplate());
$context = ['{{student_name}}' => 'Student A', '{{parent_name}}' => 'Parent A'];
$rendered = $service->renderTemplate('ABS_1', 'default', $context);
$this->assertSame('Notice for Student A', $rendered[0]);
$this->assertSame('<p>Parent A</p>', $rendered[1]);
}
public function test_pick_variant_returns_override_when_present(): void
public function test_pick_variant_fallback(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
$this->assertSame('custom_variant', $service->pickVariant('ABS_2', 'custom_variant'));
}
public function test_pick_variant_returns_no_answer_for_abs_2(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
$service = new AttendanceEmailComposerService(new \App\Models\AttendanceEmailTemplate());
$this->assertSame('no_answer', $service->pickVariant('ABS_2'));
}
public function test_pick_variant_returns_default_for_other_codes(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
$this->assertSame('default', $service->pickVariant('ABS_1'));
}
public function test_normalize_body_to_html_keeps_html_when_tags_present(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
$body = '<p>Hello</p>';
$this->assertSame($body, $service->normalizeBodyToHtml($body));
}
public function test_normalize_body_to_html_converts_plain_text_to_br_html(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
$result = $service->normalizeBodyToHtml("Hello\nWorld");
$this->assertStringContainsString('Hello', $result);
$this->assertStringContainsString('<br', $result);
$this->assertStringContainsString('World', $result);
}
public function test_build_fallback_auto_email_returns_subject_and_body(): void
{
$model = Mockery::mock(AttendanceEmailTemplate::class);
$service = new AttendanceEmailComposerService($model);
[$subject, $body] = $service->buildFallbackAutoEmail('ABS_1', [
'name' => 'John Doe',
'violation' => '1 Unreported Absence',
'last_date' => '2026-03-01',
], 'Jane Doe');
$this->assertSame('Attendance Notice: Unreported Absence', $subject);
$this->assertStringContainsString('John Doe', $body);
$this->assertStringContainsString('2026-03-01', $body);
$this->assertStringContainsString('Jane Doe', $body);
}
}
}
@@ -2,45 +2,32 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\ParentNotification;
use App\Services\AttendanceNotificationLogService;
use Mockery;
use App\Services\AttendanceTracking\AttendanceNotificationLogService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AttendanceNotificationLogServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_log_notification_creates_and_updates(): void
{
Mockery::close();
parent::tearDown();
$service = new AttendanceNotificationLogService(new \App\Models\ParentNotification());
$service->logNotification(1, 'ABS_1', '2025-01-01', 'email', 'parent@example.com', 'Subject', 'sent');
$this->assertDatabaseHas('parent_notifications', [
'student_id' => 1,
'code' => 'ABS_1',
'incident_date' => '2025-01-01',
'channel' => 'email',
'to_address' => 'parent@example.com',
'status' => 'sent',
]);
$service->logNotification(1, 'ABS_1', '2025-01-01', 'email', 'parent@example.com', 'Updated', 'failed');
$row = DB::table('parent_notifications')->where('student_id', 1)->first();
$this->assertSame('Updated', $row->subject);
$this->assertSame('failed', $row->status);
}
public function test_notification_already_sent_returns_true_when_model_has_sent_returns_true(): void
{
$model = Mockery::mock(ParentNotification::class);
$model->shouldReceive('hasSent')
->once()
->with(10, 'ABS_1', '2026-03-01', 'email', 'parent@example.com')
->andReturn(true);
$service = new AttendanceNotificationLogService($model);
$this->assertTrue(
$service->notificationAlreadySent(10, 'ABS_1', '2026-03-01', 'email', 'parent@example.com')
);
}
public function test_notification_already_sent_returns_false_when_model_has_sent_returns_false(): void
{
$model = Mockery::mock(ParentNotification::class);
$model->shouldReceive('hasSent')
->once()
->andReturn(false);
$service = new AttendanceNotificationLogService($model);
$this->assertFalse(
$service->notificationAlreadySent(10, 'ABS_1', '2026-03-01', 'email', 'parent@example.com')
);
}
}
}
@@ -2,114 +2,50 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceData;
use App\Models\AttendanceTracking;
use App\Models\Configuration;
use App\Models\Student;
use App\Models\StudentClass;
use App\Services\AttendanceEmailComposerService;
use App\Services\AttendanceMailerService;
use App\Services\AttendanceNotificationLogService;
use App\Services\AttendanceNotificationWorkflowService;
use App\Services\AttendanceParentLookupService;
use App\Services\ViolationRuleEngineService;
use App\Services\AttendanceTracking\AttendanceNotificationWorkflowService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Mockery;
use Tests\TestCase;
class AttendanceNotificationWorkflowServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_record_requires_parent_email(): void
{
Mockery::close();
parent::tearDown();
}
protected function makeService(
?Student $student = null,
?StudentClass $studentClass = null,
?AttendanceData $attendanceData = null,
?AttendanceTracking $tracking = null,
?Configuration $config = null,
?AttendanceMailerService $mailer = null,
?ViolationRuleEngineService $rules = null,
?AttendanceParentLookupService $parents = null,
?AttendanceEmailComposerService $composer = null,
?AttendanceNotificationLogService $log = null
): AttendanceNotificationWorkflowService {
$student ??= Mockery::mock(Student::class);
$studentClass ??= Mockery::mock(StudentClass::class);
$attendanceData ??= Mockery::mock(AttendanceData::class);
$tracking ??= Mockery::mock(AttendanceTracking::class);
$config ??= Mockery::mock(Configuration::class);
$mailer ??= Mockery::mock(AttendanceMailerService::class);
$rules ??= Mockery::mock(ViolationRuleEngineService::class);
$parents ??= Mockery::mock(AttendanceParentLookupService::class);
$composer ??= Mockery::mock(AttendanceEmailComposerService::class);
$log ??= Mockery::mock(AttendanceNotificationLogService::class);
$config->shouldReceive('getConfig')->with('semester')->andReturn('Fall');
$config->shouldReceive('getConfig')->with('school_year')->andReturn('2025-2026');
return new AttendanceNotificationWorkflowService(
$student,
$studentClass,
$attendanceData,
$tracking,
$config,
$mailer,
$rules,
$parents,
$composer,
$log
);
}
public function test_send_auto_emails_returns_empty_summary_when_no_auto_email_violations(): void
{
$service = $this->makeService();
$result = $service->sendAutoEmails([
['action' => 'team_notify'],
DB::table('students')->insert([
'id' => 1,
'school_id' => 'S1',
'firstname' => 'Student',
'lastname' => 'One',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
$this->assertTrue($result['success']);
$this->assertSame(0, $result['sent']);
$this->assertSame(0, $result['skipped']);
$this->assertSame(0, $result['errors']);
}
public function test_send_email_manual_returns_failure_when_all_sends_fail(): void
{
$mailer = Mockery::mock(AttendanceMailerService::class);
$mailer->shouldReceive('send')->andReturn(false);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$parents->shouldReceive('getSecondaryParentForStudent')->andReturn(null);
$composer = Mockery::mock(AttendanceEmailComposerService::class);
$composer->shouldReceive('normalizeBodyToHtml')->andReturn('<p>Hello</p>');
$composer->shouldReceive('renderWithEmailLayout')->andReturn('<html>Hello</html>');
$log = Mockery::mock(AttendanceNotificationLogService::class);
$log->shouldReceive('logNotification')->atLeast()->once();
$service = $this->makeService(
mailer: $mailer,
parents: $parents,
composer: $composer,
log: $log
$service = new AttendanceNotificationWorkflowService(
new \App\Models\Student(),
new \App\Models\StudentClass(),
new \App\Models\AttendanceData(),
new \App\Models\AttendanceTracking(),
new \App\Models\Configuration(),
Mockery::mock(\App\Services\AttendanceTracking\AttendanceMailerService::class),
Mockery::mock(\App\Services\AttendanceTracking\ViolationRuleEngineService::class),
Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class),
Mockery::mock(\App\Services\AttendanceTracking\AttendanceEmailComposerService::class),
Mockery::mock(\App\Services\AttendanceTracking\AttendanceNotificationLogService::class)
);
$result = $service->sendEmailManual([
'student_id' => 10,
'to' => 'parent@example.com',
'subject' => 'Test',
'body_html' => 'Hello',
'code' => 'ABS_1',
'incident_date' => '2026-03-01',
], ['2026-03-01']);
$result = $service->record([
'student_id' => 1,
'date' => '2025-01-01',
]);
$this->assertFalse($result['success']);
$this->assertSame(500, $result['status']);
$this->assertSame(422, $result['status']);
}
}
}
@@ -2,7 +2,7 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Services\AttendanceParentLookupService;
use App\Services\AttendanceTracking\AttendanceParentLookupService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
@@ -11,86 +11,78 @@ class AttendanceParentLookupServiceTest extends TestCase
{
use RefreshDatabase;
public function test_get_primary_parent_for_student_returns_parent_info(): void
public function test_get_primary_parent_for_student(): void
{
DB::table('users')->insert([
'id' => 100,
'firstname' => 'Jane',
'lastname' => 'Doe',
'email' => 'jane@example.com',
'cellphone' => '555-1111',
'id' => 1,
'school_id' => 1,
'firstname' => 'Parent',
'lastname' => 'One',
'cellphone' => '5555555555',
'email' => 'parent@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('students')->insert([
'id' => 200,
'parent_id' => 100,
'id' => 1,
'school_id' => 'S1',
'firstname' => 'Student',
'lastname' => 'One',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
$service = new AttendanceParentLookupService();
$parent = $service->getPrimaryParentForStudent(1);
$result = $service->getPrimaryParentForStudent(200);
$this->assertNotNull($result);
$this->assertSame(100, $result['user_id']);
$this->assertSame('jane@example.com', $result['email']);
$this->assertSame('Jane Doe', $result['parent_name']);
$this->assertSame('parent@example.com', $parent['email']);
}
public function test_get_secondary_parent_for_student_returns_secondary_user_when_present(): void
public function test_get_secondary_parent_falls_back_to_parent_row(): void
{
DB::table('users')->insert([
['id' => 100, 'firstname' => 'Jane', 'lastname' => 'Doe', 'email' => 'jane@example.com', 'cellphone' => '555-1111'],
['id' => 101, 'firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@example.com', 'cellphone' => '555-2222'],
]);
DB::table('students')->insert([
'id' => 200,
'parent_id' => 100,
'id' => 1,
'school_id' => 'S1',
'firstname' => 'Student',
'lastname' => 'One',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 11,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
DB::table('parents')->insert([
'firstparent_id' => 100,
'secondparent_id' => 101,
'secondparent_firstname' => 'Second',
'secondparent_lastname' => 'Parent',
'secondparent_gender' => 'Female',
'secondparent_email' => 'second@example.com',
'secondparent_phone' => '5551112222',
'firstparent_id' => 11,
'secondparent_id' => 0,
'semester' => 'Fall',
'school_year' => '2025-2026',
'updated_at' => now(),
]);
$service = new AttendanceParentLookupService();
$parent = $service->getSecondaryParentForStudent(1, '2025-2026');
$result = $service->getSecondaryParentForStudent(200, '2025-2026');
$this->assertNotNull($result);
$this->assertSame(101, $result['user_id']);
$this->assertSame('john@example.com', $result['email']);
$this->assertSame('second@example.com', $parent['email']);
}
public function test_parents_info_returns_primary_and_secondary_data(): void
{
DB::table('users')->insert([
['id' => 100, 'firstname' => 'Jane', 'lastname' => 'Doe', 'email' => 'jane@example.com', 'cellphone' => '555-1111'],
['id' => 101, 'firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@example.com', 'cellphone' => '555-2222'],
]);
DB::table('students')->insert([
'id' => 200,
'parent_id' => 100,
'school_year' => '2025-2026',
]);
DB::table('parents')->insert([
'firstparent_id' => 100,
'secondparent_id' => 101,
'school_year' => '2025-2026',
'updated_at' => now(),
]);
$service = new AttendanceParentLookupService();
$result = $service->parentsInfo(200, '2025-2026');
$this->assertTrue($result['success']);
$this->assertSame('jane@example.com', $result['data']['primary']['email']);
$this->assertSame('john@example.com', $result['data']['secondary']['email']);
}
}
}
@@ -2,49 +2,39 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceData;
use App\Services\AttendancePendingViolationService;
use App\Services\AttendanceViolationStudentResolverService;
use App\Services\ViolationRuleEngineService;
use App\Services\AttendanceTracking\AttendancePendingViolationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class AttendancePendingViolationServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_returns_error_when_no_students(): void
{
Mockery::close();
parent::tearDown();
}
$resolver = Mockery::mock(\App\Services\AttendanceTracking\AttendanceViolationStudentResolverService::class);
$resolver->shouldReceive('resolveForSchoolYear')->andReturn([
'school_year' => '2025-2026',
'semester' => 'Fall',
'student_ids' => [],
'student_codes' => [],
'students' => [],
'student_code_to_id' => [],
'existing_ids' => [],
'debug' => [],
]);
public function test_get_pending_violations_returns_error_when_no_student_identifiers_found(): void
{
$attendanceData = Mockery::mock(AttendanceData::class);
$rules = Mockery::mock(ViolationRuleEngineService::class);
$resolver = Mockery::mock(AttendanceViolationStudentResolverService::class);
$engine = Mockery::mock(\App\Services\AttendanceTracking\ViolationRuleEngineService::class);
$resolver->shouldReceive('resolveForSchoolYear')
->once()
->with('2025-2026', null)
->andReturn([
'school_year' => '2025-2026',
'semester' => null,
'student_ids' => [],
'student_codes' => [],
'students' => [],
'student_code_to_id' => [],
'existing_ids' => [],
'debug' => [
'class_students' => 0,
'student_ids' => 0,
],
]);
$service = new AttendancePendingViolationService(
new \App\Models\AttendanceData(),
$engine,
$resolver
);
$service = new AttendancePendingViolationService($attendanceData, $rules, $resolver);
$result = $service->getPendingViolations('2025-2026', null, null);
$result = $service->getPendingViolations('2025-2026');
$this->assertSame([], $result['students']);
$this->assertSame('No student identifiers found for the current school year.', $result['error']);
}
}
}
@@ -2,134 +2,17 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceData;
use App\Models\AttendanceTracking;
use App\Models\Configuration;
use App\Models\Student;
use App\Models\StudentClass;
use App\Services\AttendanceCaseQueryService;
use App\Services\AttendanceCommunicationSupportService;
use App\Services\AttendanceEmailComposerService;
use App\Services\AttendanceMailerService;
use App\Services\AttendanceNotificationLogService;
use App\Services\AttendanceNotificationWorkflowService;
use App\Services\AttendanceParentLookupService;
use App\Services\AttendancePendingViolationService;
use App\Services\AttendanceTrackingService;
use App\Services\AttendanceViolationStudentResolverService;
use App\Services\ViolationRuleEngineService;
use Mockery;
use App\Services\AttendanceTracking\AttendanceTrackingService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AttendanceTrackingServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_service_instantiates(): void
{
Mockery::close();
parent::tearDown();
$service = new AttendanceTrackingService();
$this->assertInstanceOf(AttendanceTrackingService::class, $service);
}
protected function makeService(
?AttendancePendingViolationService $pending = null,
?AttendanceCaseQueryService $caseQuery = null,
?AttendanceNotificationWorkflowService $workflow = null,
?AttendanceCommunicationSupportService $support = null
): AttendanceTrackingService {
$student = Mockery::mock(Student::class);
$studentClass = Mockery::mock(StudentClass::class);
$attendanceData = Mockery::mock(AttendanceData::class);
$tracking = Mockery::mock(AttendanceTracking::class);
$config = Mockery::mock(Configuration::class);
$mailer = Mockery::mock(AttendanceMailerService::class);
$rules = Mockery::mock(ViolationRuleEngineService::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$composer = Mockery::mock(AttendanceEmailComposerService::class);
$log = Mockery::mock(AttendanceNotificationLogService::class);
$resolver = Mockery::mock(AttendanceViolationStudentResolverService::class);
$pending ??= Mockery::mock(AttendancePendingViolationService::class);
$caseQuery ??= Mockery::mock(AttendanceCaseQueryService::class);
$workflow ??= Mockery::mock(AttendanceNotificationWorkflowService::class);
$support ??= Mockery::mock(AttendanceCommunicationSupportService::class);
$config->shouldReceive('getConfig')->with('semester')->andReturn('Fall');
$config->shouldReceive('getConfig')->with('school_year')->andReturn('2025-2026');
return new AttendanceTrackingService(
$student,
$studentClass,
$attendanceData,
$tracking,
$config,
$mailer,
$rules,
$parents,
$composer,
$log,
$resolver,
$caseQuery,
$workflow,
$support,
$pending
);
}
public function test_get_pending_violations_delegates_to_pending_service(): void
{
$pending = Mockery::mock(AttendancePendingViolationService::class);
$pending->shouldReceive('getPendingViolations')
->once()
->with('2025-2026', null, null)
->andReturn(['students' => []]);
$service = $this->makeService(pending: $pending);
$result = $service->getPendingViolations();
$this->assertSame(['students' => []], $result);
}
public function test_get_student_case_delegates_to_case_query_service(): void
{
$caseQuery = Mockery::mock(AttendanceCaseQueryService::class);
$caseQuery->shouldReceive('getStudentCase')
->once()
->andReturn(['success' => true]);
$service = $this->makeService(caseQuery: $caseQuery);
$result = $service->getStudentCase(1);
$this->assertTrue($result['success']);
}
public function test_record_delegates_to_workflow_service(): void
{
$workflow = Mockery::mock(AttendanceNotificationWorkflowService::class);
$workflow->shouldReceive('record')
->once()
->with(['student_id' => 10])
->andReturn(['success' => true]);
$service = $this->makeService(workflow: $workflow);
$result = $service->record(['student_id' => 10]);
$this->assertTrue($result['success']);
}
public function test_compose_delegates_to_support_service(): void
{
$support = Mockery::mock(AttendanceCommunicationSupportService::class);
$support->shouldReceive('compose')
->once()
->with(10, 'ABS_1', 'default', null)
->andReturn(['success' => true]);
$service = $this->makeService(support: $support);
$result = $service->compose(10);
$this->assertTrue($result['success']);
}
}
}
@@ -2,67 +2,46 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceData;
use App\Models\Student;
use App\Models\StudentClass;
use App\Services\AttendanceViolationStudentResolverService;
use Mockery;
use App\Services\AttendanceTracking\AttendanceViolationStudentResolverService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AttendanceViolationStudentResolverServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_resolve_for_school_year_returns_students(): void
{
Mockery::close();
parent::tearDown();
}
DB::table('students')->insert([
'id' => 1,
'school_id' => 'S1',
'firstname' => 'Student',
'lastname' => 'One',
'age' => 8,
'gender' => 'Male',
'photo_consent' => 1,
'parent_id' => 1,
'year_of_registration' => '2025',
'school_year' => '2025-2026',
]);
public function test_ensure_attendance_student_exists_keeps_numeric_student(): void
{
$student = Mockery::mock(Student::class);
$studentClass = Mockery::mock(StudentClass::class);
$attendanceData = Mockery::mock(AttendanceData::class);
DB::table('student_class')->insert([
'student_id' => 1,
'class_section_id' => 10,
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$service = new AttendanceViolationStudentResolverService($student, $studentClass, $attendanceData);
$students = [];
$studentCodeToId = [];
$existingIds = [];
$sid = $service->ensureAttendanceStudentExists(
$students,
$studentCodeToId,
$existingIds,
123
$service = new AttendanceViolationStudentResolverService(
new \App\Models\Student(),
new \App\Models\StudentClass(),
new \App\Models\AttendanceData()
);
$this->assertSame(123, $sid);
$this->assertCount(1, $students);
$this->assertSame(123, $students[0]['id']);
$result = $service->resolveForSchoolYear('2025-2026', 'Fall');
$this->assertContains(1, $result['student_ids']);
$this->assertNotEmpty($result['students']);
}
public function test_ensure_attendance_student_exists_creates_placeholder_for_code(): void
{
$student = Mockery::mock(Student::class);
$studentClass = Mockery::mock(StudentClass::class);
$attendanceData = Mockery::mock(AttendanceData::class);
$service = new AttendanceViolationStudentResolverService($student, $studentClass, $attendanceData);
$students = [];
$studentCodeToId = [];
$existingIds = [];
$sid = $service->ensureAttendanceStudentExists(
$students,
$studentCodeToId,
$existingIds,
'SCH-001'
);
$this->assertNotNull($sid);
$this->assertCount(1, $students);
$this->assertSame('SCH-001', $students[0]['school_id']);
$this->assertArrayHasKey('SCH-001', $studentCodeToId);
}
}
}
@@ -2,111 +2,37 @@
namespace Tests\Unit\Services\AttendanceTracking;
use App\Models\AttendanceTracking;
use App\Services\AttendanceParentLookupService;
use App\Services\ViolationRuleEngineService;
use App\Services\AttendanceTracking\ViolationRuleEngineService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class ViolationRuleEngineServiceTest extends TestCase
{
protected function tearDown(): void
use RefreshDatabase;
public function test_school_year_helpers(): void
{
Mockery::close();
parent::tearDown();
$service = $this->makeService();
$this->assertSame(['2025-08-01', '2026-07-31'], $service->deriveSchoolYearBounds('2025-2026'));
$this->assertSame('2025-2026', $service->schoolYearForDate('2025-09-01'));
}
public function test_choose_higher_severity_returns_higher_severity_rule(): void
public function test_window_weeks_for_violation_code(): void
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
$result = $service->chooseHigherSeverity(
['severity' => 2, 'action' => 'team_notify'],
['severity' => 4, 'action' => 'auto_email']
);
$this->assertSame(4, $result['severity']);
}
public function test_choose_higher_severity_breaks_ties_by_action_priority(): void
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
$result = $service->chooseHigherSeverity(
['severity' => 3, 'action' => 'team_notify'],
['severity' => 3, 'action' => 'auto_email']
);
$this->assertSame('team_notify', $result['action']);
}
public function test_window_weeks_for_violation_code_returns_expected_values(): void
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
$service = $this->makeService();
$this->assertSame(3, $service->windowWeeksForViolationCode('ABS_2'));
$this->assertSame(4, $service->windowWeeksForViolationCode('ABS_3'));
$this->assertSame(4, $service->windowWeeksForViolationCode('MIX'));
$this->assertSame(5, $service->windowWeeksForViolationCode('ABS_4'));
$this->assertSame(4, $service->windowWeeksForViolationCode('LATE_4'));
}
public function test_has_n_consecutive_items_detects_weekly_sequence(): void
private function makeService(): ViolationRuleEngineService
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$tracking = new \App\Models\AttendanceTracking();
$parentLookup = Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
$result = $service->hasNConsecutiveItems(
['2026-03-15', '2026-03-08', '2026-03-01'],
3,
7
);
$this->assertTrue($result);
return new ViolationRuleEngineService($tracking, $parentLookup);
}
public function test_has_n_in_w_active_weeks_detects_window_match(): void
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
$this->assertTrue($service->hasNInWActiveWeeks([0, 1], 2, 3));
$this->assertFalse($service->hasNInWActiveWeeks([0], 2, 3));
}
public function test_two_lates_one_abs_in_w_weeks_detects_mix_rule(): void
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
$this->assertTrue($service->twoLatesOneAbsInWWeeks([1, 2], [3], 4));
$this->assertFalse($service->twoLatesOneAbsInWWeeks([1], [3], 4));
}
public function test_derive_school_year_bounds_returns_expected_dates(): void
{
$tracking = Mockery::mock(AttendanceTracking::class);
$parents = Mockery::mock(AttendanceParentLookupService::class);
$service = new ViolationRuleEngineService($tracking, $parents);
[$start, $end] = $service->deriveSchoolYearBounds('2025-2026');
$this->assertSame('2025-08-01', $start);
$this->assertSame('2026-07-31', $end);
}
}
}