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