apply the school year concept
Tests / PHPUnit (push) Failing after 1m19s

This commit is contained in:
root
2026-07-14 00:59:00 -04:00
parent 504c3bc9f9
commit feb1b29a32
73 changed files with 4288 additions and 620 deletions
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Services;
final class SchoolYearAccessPolicy
{
public function canView(?int $userId, array $year): bool
{
if (($year['status'] ?? '') !== 'draft') {
return true;
}
return $this->canViewDraft($userId);
}
public function canSelect(?int $userId, array $year): bool
{
return $this->canView($userId, $year);
}
public function canViewDraft(?int $userId): bool
{
if ($userId === null || $userId <= 0) {
return false;
}
$roles = array_map(
static fn ($role): string => strtolower(trim((string) $role)),
(array) session()->get('roles')
);
return (bool) array_intersect($roles, [
'admin',
'administrator',
'administrative staff',
'principal',
]);
}
}
+385 -3
View File
@@ -5,6 +5,7 @@ namespace App\Services;
use App\Models\SchoolYearClosingBatchModel;
use App\Models\SchoolYearClosingItemModel;
use App\Models\SchoolYearModel;
use App\Models\ConfigurationModel;
use App\Support\SchoolYear\SchoolYearStatus;
use CodeIgniter\Database\BaseConnection;
use InvalidArgumentException;
@@ -16,6 +17,7 @@ final class SchoolYearClosingService
private readonly SchoolYearModel $schoolYearModel,
private readonly SchoolYearClosingBatchModel $batchModel,
private readonly SchoolYearClosingItemModel $itemModel,
private readonly ConfigurationModel $configurationModel,
private readonly SchoolYearManagementService $managementService,
private readonly BaseConnection $db,
) {
@@ -50,6 +52,29 @@ final class SchoolYearClosingService
$findings[] = $finding;
}
$promotion = $this->promotionPreview($sourceName, $target !== null ? (string) ($target['name'] ?? '') : null);
if (($promotion['summary']['missing_decision'] ?? 0) > 0) {
$findings[] = $this->finding(
'blocking',
'Students missing promotion decisions',
$promotion['summary']['missing_decision'] . ' active student(s) do not have a saved promotion decision for this school year.'
);
}
if (($promotion['summary']['pending_decision'] ?? 0) > 0) {
$findings[] = $this->finding(
'blocking',
'Students with pending promotion decisions',
$promotion['summary']['pending_decision'] . ' active student(s) still have pending promotion decisions.'
);
}
if (($promotion['summary']['missing_queue'] ?? 0) > 0) {
$findings[] = $this->finding(
'warning',
'Passed students missing promotion queue',
$promotion['summary']['missing_queue'] . ' passed student(s) are not queued for the target school year. This will not block closing.'
);
}
if ($this->countMissingSchoolYearRows('invoices') > 0) {
$findings[] = $this->finding('blocking', 'Invoices missing school year', 'Some invoice records are not assigned to a school year.');
}
@@ -63,6 +88,7 @@ final class SchoolYearClosingService
'target' => $target,
'overview' => $overview,
'finance' => $finance,
'promotion' => $promotion,
'findings' => $findings,
'blockers' => $blockers,
'warnings' => $warnings,
@@ -183,6 +209,12 @@ final class SchoolYearClosingService
throw new InvalidArgumentException('All closing batch items must complete before the year can be closed.');
}
$targetYearId = (int) ($batch['target_school_year_id'] ?? 0);
$target = $targetYearId > 0 ? $this->schoolYearModel->find($targetYearId) : null;
if ($target === null) {
throw new InvalidArgumentException('A target school year is required before completing closing.');
}
$this->db->transStart();
$now = date('Y-m-d H:i:s');
$this->batchModel->update((int) $batch['id'], [
@@ -195,8 +227,25 @@ final class SchoolYearClosingService
'closed_at' => $now,
'updated_by' => $userId,
]);
$this->schoolYearModel->update($targetYearId, [
'status' => SchoolYearStatus::ACTIVE,
'previous_school_year_id' => $sourceYearId,
'activated_at' => $target['activated_at'] ?? $now,
'updated_by' => $userId,
]);
$this->schoolYearModel->update($sourceYearId, [
'next_school_year_id' => $targetYearId,
]);
$targetName = (string) ($target['name'] ?? '');
$this->configurationModel->setConfigValueByKey('school_year', $targetName);
$this->syncActiveYearSession($targetName);
$this->managementService->log($sourceYearId, SchoolYearStatus::CLOSING, SchoolYearStatus::CLOSED, 'closing_complete', $userId, [
'closing_batch_id' => (int) $batch['id'],
'activated_school_year_id' => $targetYearId,
]);
$this->managementService->log($targetYearId, (string) ($target['status'] ?? SchoolYearStatus::DRAFT), SchoolYearStatus::ACTIVE, 'activate_after_closing', $userId, [
'closed_school_year_id' => $sourceYearId,
'closing_batch_id' => (int) $batch['id'],
]);
$this->db->transComplete();
@@ -322,11 +371,21 @@ final class SchoolYearClosingService
return [];
}
$rows = $this->db->table('invoices i')
$builder = $this->db->table('invoices i')
->select('i.parent_id AS family_id')
->select('COALESCE(SUM(i.balance), 0) AS source_balance')
->where('i.school_year', $schoolYear)
->groupBy('i.parent_id')
->where('i.school_year', $schoolYear);
if ($this->db->tableExists('users')) {
$builder
->select('u.firstname AS parent_firstname, u.lastname AS parent_lastname, u.email AS parent_email')
->join('users u', 'u.id = i.parent_id', 'left')
->groupBy('i.parent_id, u.firstname, u.lastname, u.email');
} else {
$builder->groupBy('i.parent_id');
}
$rows = $builder
->having('source_balance !=', 0)
->orderBy('i.parent_id', 'ASC')
->get()
@@ -338,6 +397,8 @@ final class SchoolYearClosingService
return [
'family_id' => (int) $row['family_id'],
'family' => 'Family #' . (int) $row['family_id'],
'parent' => trim((string) ($row['parent_firstname'] ?? '') . ' ' . (string) ($row['parent_lastname'] ?? '')) ?: 'Parent #' . (int) $row['family_id'],
'parent_email' => (string) ($row['parent_email'] ?? ''),
'source_balance' => $balance,
'credit_amount' => $balance < 0 ? abs($balance) : 0.0,
'adjustment_amount' => 0.0,
@@ -346,6 +407,315 @@ final class SchoolYearClosingService
}, $rows);
}
private function promotionPreview(string $schoolYear, ?string $targetSchoolYear): array
{
$summary = [
'total_students' => 0,
'with_decision' => 0,
'missing_decision' => 0,
'pending_decision' => 0,
'pass' => 0,
'other_decision' => 0,
'queued' => 0,
'assigned' => 0,
'applied' => 0,
'missing_queue' => 0,
];
if (! $this->db->tableExists('student_class') || ! $this->db->tableExists('students')) {
return [
'summary' => $summary,
'rows' => [],
];
}
$hasEventOnly = $this->db->fieldExists('is_event_only', 'student_class');
$hasActive = $this->db->fieldExists('is_active', 'students');
$hasDob = $this->db->fieldExists('dob', 'students');
$hasRegistrationGrade = $this->db->fieldExists('registration_grade', 'students');
$builder = $this->db->table('student_class sc')
->select('sc.student_id, sc.class_section_id, sc.created_at, sc.updated_at')
->select('s.school_id, s.firstname, s.lastname')
->select('cs.class_section_name')
->join('students s', 's.id = sc.student_id', 'inner')
->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left')
->where('sc.school_year', $schoolYear)
->where('sc.class_section_id IS NOT NULL', null, false);
if ($hasDob) {
$builder->select('s.dob');
}
if ($hasRegistrationGrade) {
$builder->select('s.registration_grade');
}
if ($hasEventOnly) {
$builder->groupStart()
->where('sc.is_event_only', 0)
->orWhere('sc.is_event_only', null)
->groupEnd();
}
if ($hasActive) {
$builder->where('s.is_active', 1);
}
$assignmentRows = $builder
->orderBy('sc.student_id', 'ASC')
->orderBy('sc.updated_at', 'DESC')
->orderBy('sc.created_at', 'DESC')
->get()
->getResultArray();
$students = [];
foreach ($assignmentRows as $row) {
$studentId = (int) ($row['student_id'] ?? 0);
if ($studentId <= 0 || isset($students[$studentId])) {
continue;
}
$students[$studentId] = [
'student_id' => $studentId,
'school_id' => (string) ($row['school_id'] ?? ''),
'student_name' => trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')),
'class_section_id' => (int) ($row['class_section_id'] ?? 0),
'class_section_name' => (string) ($row['class_section_name'] ?? ''),
'dob' => (string) ($row['dob'] ?? ''),
'registration_grade' => (string) ($row['registration_grade'] ?? ''),
'auto_kg_pass' => false,
'year_score' => null,
'decision' => '',
'source' => 'missing',
'notes' => '',
'status' => 'missing',
'queue_status' => '',
'target_class' => '',
'target_section' => '',
];
}
if ($students === []) {
return [
'summary' => $summary,
'rows' => [],
];
}
$decisions = $this->promotionDecisionRows(array_keys($students), $schoolYear);
$queueRows = $this->promotionQueueRows(array_keys($students), $schoolYear, $targetSchoolYear);
foreach ($students as $studentId => &$student) {
$decision = $decisions[$studentId] ?? null;
if ($decision !== null) {
$student['year_score'] = $decision['year_score'];
$student['decision'] = $decision['decision'];
$student['source'] = $decision['source'];
$student['notes'] = $decision['notes'];
$student['status'] = $decision['status'];
if (($decision['class_section_name'] ?? '') !== '') {
$student['class_section_name'] = $decision['class_section_name'];
}
}
if ($decision === null && $this->isKgStudent($student)) {
$kgAgeStatus = $this->kgAgeStatusByTargetYearCutoff((string) $student['dob'], $targetSchoolYear);
if ($kgAgeStatus === 'pass') {
$student['decision'] = 'Pass';
$student['source'] = 'automatic_kg_age';
$student['notes'] = 'Auto-pass KG student: age 6 or older by Dec 31 of the next school year start year.';
$student['status'] = 'decided';
$student['auto_kg_pass'] = true;
} elseif ($kgAgeStatus === 'keep_kg') {
$student['decision'] = 'Keep KG';
$student['source'] = 'automatic_kg_age';
$student['notes'] = 'Auto-keep KG student: younger than 6 by Dec 31 of the next school year start year.';
$student['status'] = 'decided';
}
}
$queue = $queueRows[$studentId] ?? null;
if ($queue !== null) {
$student['queue_status'] = $queue['status'];
$student['target_class'] = $queue['target_class'];
$student['target_section'] = $queue['target_section'];
}
$summary['total_students']++;
if ($student['status'] === 'missing') {
$summary['missing_decision']++;
} elseif ($student['status'] === 'pending') {
$summary['pending_decision']++;
} else {
$summary['with_decision']++;
if (strcasecmp((string) $student['decision'], 'Pass') === 0) {
$summary['pass']++;
if ($queue === null && $student['auto_kg_pass'] !== true) {
$summary['missing_queue']++;
}
} else {
$summary['other_decision']++;
}
}
if ($queue !== null && isset($summary[$queue['status']])) {
$summary[$queue['status']]++;
}
}
unset($student);
$rows = array_values($students);
usort($rows, static function (array $a, array $b): int {
return [$a['class_section_name'], $a['student_name'], $a['student_id']]
<=> [$b['class_section_name'], $b['student_name'], $b['student_id']];
});
return [
'summary' => $summary,
'rows' => $rows,
];
}
private function isKgStudent(array $student): bool
{
foreach (['class_section_name', 'registration_grade'] as $field) {
$value = strtoupper(trim((string) ($student[$field] ?? '')));
if ($value === 'KG' || str_starts_with($value, 'KG-') || str_contains($value, 'KINDERGARTEN')) {
return true;
}
}
return false;
}
private function kgAgeStatusByTargetYearCutoff(string $dob, ?string $targetSchoolYear): string
{
$dob = trim($dob);
if ($dob === '' || $targetSchoolYear === null || ! preg_match('/^(\d{4})-\d{4}$/', $targetSchoolYear, $matches)) {
return '';
}
try {
$birthDate = new \DateTimeImmutable($dob);
$cutoff = new \DateTimeImmutable($matches[1] . '-12-31');
} catch (\Throwable) {
return '';
}
if ($birthDate > $cutoff) {
return '';
}
$age = $birthDate->diff($cutoff)->y;
if ($age >= 6) {
return 'pass';
}
return 'keep_kg';
}
private function promotionDecisionRows(array $studentIds, string $schoolYear): array
{
if (! $this->db->tableExists('student_decisions')) {
return [];
}
$select = ['student_id', 'class_section_name', 'decision', 'source', 'notes'];
$scoreField = null;
if ($this->db->fieldExists('year_score', 'student_decisions')) {
$scoreField = 'year_score';
} elseif ($this->db->fieldExists('semester_score', 'student_decisions')) {
$scoreField = 'semester_score';
}
if ($scoreField !== null) {
$select[] = $scoreField . ' AS year_score';
}
$rows = $this->db->table('student_decisions')
->select($select)
->where('school_year', $schoolYear)
->whereIn('student_id', $studentIds)
->orderBy('updated_at', 'DESC')
->orderBy('id', 'DESC')
->get()
->getResultArray();
$decisions = [];
foreach ($rows as $row) {
$studentId = (int) ($row['student_id'] ?? 0);
if ($studentId <= 0 || isset($decisions[$studentId])) {
continue;
}
$decision = trim((string) ($row['decision'] ?? ''));
$source = trim((string) ($row['source'] ?? ''));
$status = $decision === '' || $source === 'pending' ? 'pending' : 'decided';
$decisions[$studentId] = [
'class_section_name' => (string) ($row['class_section_name'] ?? ''),
'year_score' => is_numeric($row['year_score'] ?? null) ? round((float) $row['year_score'], 2) : null,
'decision' => $decision,
'source' => $source !== '' ? $source : ($status === 'pending' ? 'pending' : 'manual'),
'notes' => (string) ($row['notes'] ?? ''),
'status' => $status,
];
}
return $decisions;
}
private function promotionQueueRows(array $studentIds, string $sourceSchoolYear, ?string $targetSchoolYear): array
{
if (
$targetSchoolYear === null
|| $targetSchoolYear === ''
|| ! $this->db->tableExists('promotion_queue')
) {
return [];
}
$builder = $this->db->table('promotion_queue pq')
->select('pq.student_id, pq.to_class_id, pq.to_class_section_id, pq.status')
->select('c.class_name AS target_class')
->select('cs.class_section_name AS target_section')
->join('classes c', 'c.id = pq.to_class_id', 'left')
->join('classSection cs', 'cs.class_section_id = pq.to_class_section_id', 'left')
->where('pq.school_year_from', $sourceSchoolYear)
->where('pq.school_year_to', $targetSchoolYear)
->whereIn('pq.student_id', $studentIds)
->orderBy('pq.updated_at', 'DESC')
->orderBy('pq.id', 'DESC');
$rows = $builder->get()->getResultArray();
$queueRows = [];
foreach ($rows as $row) {
$studentId = (int) ($row['student_id'] ?? 0);
if ($studentId <= 0 || isset($queueRows[$studentId])) {
continue;
}
$targetClass = trim((string) ($row['target_class'] ?? ''));
if ($targetClass === '' && (int) ($row['to_class_id'] ?? 0) > 0) {
$targetClass = 'Class #' . (int) $row['to_class_id'];
}
$targetSection = trim((string) ($row['target_section'] ?? ''));
if ($targetSection === '' && (int) ($row['to_class_section_id'] ?? 0) > 0) {
$targetSection = 'Section #' . (int) $row['to_class_section_id'];
}
$queueRows[$studentId] = [
'status' => (string) ($row['status'] ?? ''),
'target_class' => $targetClass,
'target_section' => $targetSection,
];
}
return $queueRows;
}
private function countBySchoolYear(string $table, string $schoolYear, string $distinctField): int
{
if (! $this->db->tableExists($table) || ! $this->db->fieldExists('school_year', $table)) {
@@ -423,8 +793,20 @@ final class SchoolYearClosingService
'source_id' => $preview['source']['id'] ?? null,
'target_id' => $preview['target']['id'] ?? null,
'finance' => $preview['finance'],
'promotion' => $preview['promotion'],
'carry_forward' => $preview['carry_forward'],
'blockers' => $preview['blockers'],
], JSON_UNESCAPED_SLASHES));
}
private function syncActiveYearSession(string $schoolYearName): void
{
if (session_status() !== PHP_SESSION_ACTIVE) {
return;
}
session()->set('school_year', $schoolYearName);
session()->remove('selected_school_year_id');
session()->remove('selected_school_year');
}
}
+125 -15
View File
@@ -2,15 +2,19 @@
namespace App\Services;
use App\Exceptions\SchoolYear\InvalidSchoolYearSelectionException;
use App\Exceptions\SchoolYear\SchoolYearConfigurationException;
use App\Exceptions\SchoolYear\SchoolYearNotFoundException;
use App\Models\SchoolYearModel;
use App\Support\SchoolYear\SchoolYearStatus;
use App\Support\SchoolYear\SchoolYearContext;
use CodeIgniter\HTTP\IncomingRequest;
use RuntimeException;
final class SchoolYearContextService
{
public function __construct(
private readonly SchoolYearModel $schoolYearModel,
private readonly SchoolYearAccessPolicy $accessPolicy = new SchoolYearAccessPolicy(),
) {
}
@@ -18,29 +22,28 @@ final class SchoolYearContextService
IncomingRequest $request,
?int $routeSchoolYearId = null
): SchoolYearContext {
$requestedId = $routeSchoolYearId
?? $this->normalizeInt($request->getGet('school_year_id'));
$requestedName = trim((string) $request->getGet('school_year'));
$requestedId = $routeSchoolYearId ?? $this->normalizeInt($request->getGet('school_year_id'));
$requestedName = $this->legacyYearName($request);
$userId = (int) (session()->get('user_id') ?? 0);
if ($requestedId !== null && $requestedName !== '') {
$row = $this->schoolYearModel->find($requestedId);
if ($row === null || (string) ($row['name'] ?? '') !== $requestedName) {
throw new RuntimeException('Selected school-year parameters conflict.');
throw new InvalidSchoolYearSelectionException('Selected school-year parameters conflict.');
}
return $this->fromRow($row, true);
return $this->authorizedContext($row, $userId, true);
}
if ($requestedId !== null) {
$row = $this->schoolYearModel->find($requestedId);
if ($row === null) {
throw new RuntimeException('Selected school year was not found.');
throw new SchoolYearNotFoundException('Selected school year was not found.');
}
return $this->fromRow($row, true);
return $this->authorizedContext($row, $userId, true);
}
if ($requestedName !== '') {
@@ -49,10 +52,10 @@ final class SchoolYearContextService
->first();
if ($row === null) {
throw new RuntimeException('Selected school year was not found.');
throw new SchoolYearNotFoundException('Selected school year was not found.');
}
return $this->fromRow($row, true);
return $this->authorizedContext($row, $userId, true);
}
$sessionYearId = session('selected_school_year_id');
@@ -60,15 +63,73 @@ final class SchoolYearContextService
if (is_numeric($sessionYearId)) {
$row = $this->schoolYearModel->find((int) $sessionYearId);
if ($row !== null) {
return $this->fromRow($row, false);
if ($row !== null && $this->isSelectable($row, $userId)) {
return $this->fromRow($row, true);
}
$this->clearSelection();
}
return $this->active();
}
public function selectableYears(bool $includeDraft = false): array
{
$builder = $this->schoolYearModel
->orderBy('name', 'DESC')
->orderBy('id', 'DESC');
if (! $includeDraft) {
$builder->where('status !=', SchoolYearStatus::DRAFT);
}
$userId = (int) (session()->get('user_id') ?? 0);
return array_values(array_filter(
$builder->findAll(),
fn (array $row): bool => $this->isSelectable($row, $userId)
));
}
public function select(int $schoolYearId): SchoolYearContext
{
$row = $this->schoolYearModel->find($schoolYearId);
if ($row === null) {
throw new SchoolYearNotFoundException('Selected school year was not found.');
}
$userId = (int) (session()->get('user_id') ?? 0);
$context = $this->authorizedContext($row, $userId, true);
$previousSelection = session('selected_school_year_id');
if ($context->isActive()) {
$this->clearSelection();
$context = $this->fromRow($row, false);
} else {
session()->set('selected_school_year_id', $context->id());
session()->remove('selected_school_year');
}
if ((string) $previousSelection !== (string) session('selected_school_year_id')) {
$this->clearDependentSessionState();
}
return $context;
}
public function clearSelection(): void
{
session()->remove('selected_school_year_id');
session()->remove('selected_school_year');
}
public function active(): SchoolYearContext
{
$active = $this->schoolYearModel->active();
if ($active === null) {
throw new RuntimeException('No active school year is configured.');
throw new SchoolYearConfigurationException('Exactly one active school year must be configured.');
}
return $this->fromRow($active, false);
@@ -83,12 +144,61 @@ final class SchoolYearContextService
return (int) $value;
}
private function legacyYearName(IncomingRequest $request): string
{
$names = [];
foreach (['school_year', 'schoolYear', 'year'] as $key) {
$value = trim((string) ($request->getGet($key) ?? ''));
if ($value !== '') {
$names[$key] = $value;
}
}
if (count(array_unique($names)) > 1) {
throw new InvalidSchoolYearSelectionException('Conflicting school-year parameters were provided.');
}
if (isset($names['schoolYear']) || isset($names['year'])) {
log_message('notice', 'Legacy school-year request alias used: {aliases}', [
'aliases' => implode(',', array_keys($names)),
]);
}
return $names === [] ? '' : (string) reset($names);
}
private function authorizedContext(array $row, int $userId, bool $explicit): SchoolYearContext
{
if (! $this->isSelectable($row, $userId)) {
throw new SchoolYearNotFoundException('Selected school year was not found.');
}
return $this->fromRow($row, $explicit);
}
private function isSelectable(array $row, int $userId): bool
{
return $this->accessPolicy->canSelect($userId > 0 ? $userId : null, $row);
}
private function clearDependentSessionState(): void
{
session()->remove([
'class_section_id',
'semester',
'active_semester',
'teacher_scores_selected_semester',
'grading_selected_semester',
]);
}
private function fromRow(array $row, bool $explicit): SchoolYearContext
{
return new SchoolYearContext(
id: (int) $row['id'],
yearName: (string) $row['name'],
status: (string) $row['status'],
status: strtolower((string) $row['status']),
explicitSelection: $explicit,
);
}
@@ -105,6 +105,7 @@ final class SchoolYearManagementService
'updated_by' => $userId,
]);
$this->configurationModel->setConfigValueByKey('school_year', (string) $year['name']);
$this->syncActiveYearSession((string) $year['name']);
$this->log($id, $from, SchoolYearStatus::ACTIVE, 'activate', $userId);
$this->db->transComplete();
@@ -310,4 +311,15 @@ final class SchoolYearManagementService
return is_string($first) && $first !== '' ? $first : $fallback;
}
private function syncActiveYearSession(string $schoolYearName): void
{
if (session_status() !== PHP_SESSION_ACTIVE) {
return;
}
session()->set('school_year', $schoolYearName);
session()->remove('selected_school_year_id');
session()->remove('selected_school_year');
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Services;
use CodeIgniter\HTTP\IncomingRequest;
final class SchoolYearViewDataService
{
public function __construct(
private readonly SchoolYearContextService $contextService,
private readonly SchoolYearAccessPolicy $accessPolicy,
) {
}
public function forCurrentRequest(IncomingRequest $request): array
{
$context = $this->contextService->resolve($request);
$userId = (int) (session()->get('user_id') ?? 0);
return [
'schoolYearContext' => $context,
'schoolYearOptions' => $this->contextService->selectableYears(
$this->accessPolicy->canViewDraft($userId)
),
'schoolYearReadonly' => $context->isReadonly(),
'schoolYearSelectorEnabled' => config('Feature')->globalSchoolYearSelector,
];
}
}
+4 -4
View File
@@ -2,8 +2,8 @@
namespace App\Services;
use App\Exceptions\SchoolYear\SchoolYearWriteConflictException;
use App\Support\SchoolYear\SchoolYearContext;
use RuntimeException;
final class SchoolYearWriteGuard
{
@@ -12,14 +12,14 @@ final class SchoolYearWriteGuard
bool $allowDraftForAdmin = false,
bool $isAdmin = false
): void {
if ($context->status() === 'active') {
if (strtolower($context->status()) === 'active') {
return;
}
if ($context->status() === 'draft' && $allowDraftForAdmin && $isAdmin) {
if (strtolower($context->status()) === 'draft' && $allowDraftForAdmin && $isAdmin) {
return;
}
throw new RuntimeException('The selected school year is read-only.');
throw new SchoolYearWriteConflictException('The selected school year is read-only.');
}
}