fix enrollment logic, add financial aid, fix class distribution
Tests / PHPUnit (push) Failing after 1m6s

This commit is contained in:
root
2026-08-15 15:07:16 -04:00
parent c12bb59372
commit 4603d9ced2
95 changed files with 6892 additions and 1295 deletions
+189 -9
View File
@@ -8,6 +8,7 @@ use App\Models\SchoolYearModel;
use App\Models\SchoolYearTransitionLogModel;
use App\Support\SchoolYear\SchoolYearStatus;
use CodeIgniter\Database\BaseConnection;
use DateTimeImmutable;
use InvalidArgumentException;
use RuntimeException;
@@ -25,15 +26,29 @@ final class SchoolYearManagementService
public function createDraft(array $payload, ?int $userId = null): int
{
$existingYears = $this->db->table($this->schoolYearModel->getTable())->countAllResults();
$nextDraft = $this->nextDraftDefaults();
if ($nextDraft['name'] === null) {
throw new InvalidArgumentException('Create an initial school year before using automatic next-year draft creation.');
$requestedName = trim((string) ($payload['name'] ?? ''));
if ($existingYears === 0) {
if (! preg_match('/^\d{4}-\d{4}$/', $requestedName)) {
throw new InvalidArgumentException('Enter the first school year as YYYY-YYYY, for example 2025-2026.');
}
$payload['name'] = $requestedName;
} else {
if ($nextDraft['name'] === null) {
throw new InvalidArgumentException('Create an initial school year before using automatic next-year draft creation.');
}
$payload['name'] = $nextDraft['name'];
}
$payload['name'] = $nextDraft['name'];
$payload = $this->withCalendarDefaults($payload, (string) $payload['name']);
$payload = $this->metadataPayload($payload);
$payload['previous_school_year_id'] = (int) ($nextDraft['previous_year']['id'] ?? 0) ?: $this->previousYearIdForDraft((string) $payload['name']);
$payload['previous_school_year_id'] = $existingYears === 0
? null
: ((int) ($nextDraft['previous_year']['id'] ?? 0) ?: $this->previousYearIdForDraft((string) $payload['name']));
$payload['status'] = SchoolYearStatus::DRAFT;
$payload['carry_over_balance_behavior'] = 'submission_blocked_until_payment';
$payload['created_by'] = $userId;
$payload['updated_by'] = $userId;
@@ -42,7 +57,9 @@ final class SchoolYearManagementService
$this->db->transStart();
$id = $this->schoolYearModel->insert($payload, true);
if ($id !== false) {
$this->syncConfigurationFromSchoolYear($payload);
if ($existingYears === 0 || $this->schoolYearModel->active() === null) {
$this->syncConfigurationFromSchoolYear($payload);
}
$this->log((int) $id, null, SchoolYearStatus::DRAFT, 'create', $userId);
}
$this->db->transComplete();
@@ -54,6 +71,49 @@ final class SchoolYearManagementService
return (int) $id;
}
public function ensureNextDraftForClosing(int $sourceYearId, ?int $userId = null): array
{
$source = $this->requireYear($sourceYearId);
$sourceName = (string) ($source['name'] ?? '');
$nextName = $this->nextSchoolYearName($sourceName);
if ($nextName === null) {
throw new InvalidArgumentException('Unable to determine the next school year name.');
}
$existing = $this->schoolYearModel
->where('name', $nextName)
->first();
if ($existing !== null) {
return $existing;
}
$payload = $this->metadataPayload($this->withCalendarDefaults([
'name' => $nextName,
'previous_school_year_id' => $sourceYearId,
], $nextName));
$payload['status'] = SchoolYearStatus::DRAFT;
$payload['carry_over_balance_behavior'] = 'submission_blocked_until_payment';
$payload['created_by'] = $userId;
$payload['updated_by'] = $userId;
$this->validationService->validateMetadata($payload);
$this->db->transStart();
$id = $this->schoolYearModel->insert($payload, true);
if ($id !== false) {
$this->log((int) $id, null, SchoolYearStatus::DRAFT, 'create_for_closing', $userId, [
'source_school_year_id' => $sourceYearId,
]);
}
$this->db->transComplete();
if ($id === false || $this->db->transStatus() === false) {
throw new RuntimeException($this->firstModelError('Unable to create next school year.'));
}
return $this->requireYear((int) $id);
}
public function nextDraftDefaults(): array
{
$previousYear = $this->sourceYearForNextDraft();
@@ -83,7 +143,9 @@ final class SchoolYearManagementService
$this->db->transStart();
$updated = $this->schoolYearModel->update($id, $payload);
if ($updated !== false) {
$this->syncConfigurationFromSchoolYear($payload);
if ($status === SchoolYearStatus::ACTIVE) {
$this->syncConfigurationFromSchoolYear(array_merge($year, $payload));
}
$this->log($id, $status, $status, 'metadata_update', $userId);
}
$this->db->transComplete();
@@ -121,10 +183,13 @@ final class SchoolYearManagementService
]);
}
$year['carry_over_balance_behavior'] = trim((string) ($year['carry_over_balance_behavior'] ?? ''))
?: 'submission_blocked_until_payment';
$this->schoolYearModel->update($id, [
'status' => SchoolYearStatus::ACTIVE,
'activated_at' => $now,
'updated_by' => $userId,
'carry_over_balance_behavior' => $year['carry_over_balance_behavior'],
]);
$this->syncConfigurationFromSchoolYear($year);
$this->syncActiveYearSession((string) $year['name']);
@@ -137,6 +202,11 @@ final class SchoolYearManagementService
}
}
public function syncConfigurationForYear(int $id): void
{
$this->syncConfigurationFromSchoolYear($this->requireYear($id));
}
public function deleteDraft(int $id, ?int $userId = null): void
{
$year = $this->requireYear($id);
@@ -289,14 +359,54 @@ final class SchoolYearManagementService
throw new RuntimeException('Unable to update school-year configuration: school year name is missing.');
}
$calendar = $this->calendarPayloadForSchoolYear($name);
$schoolYear = $this->withCalendarDefaults($schoolYear, $name);
$ageReferenceDate = $this->ageReferenceDateForSchoolYear($name);
$yearStart = (string) ($schoolYear['starts_on'] ?? $calendar['starts_on']);
$yearEnd = (string) ($schoolYear['ends_on'] ?? $calendar['ends_on']);
$registrationDay = (string) ($schoolYear['registration_starts_on'] ?? $calendar['registration_starts_on']);
$enrollmentDeadline = (string) ($schoolYear['registration_ends_on'] ?? $calendar['registration_ends_on']);
$firstDay = $calendar['first_day_of_school'];
$lastDay = $calendar['last_day_of_school'];
$finalExam = $calendar['final_exam_day'];
$makeupExam = (string) ($schoolYear['fall_makeup_exam_on'] ?? $calendar['fall_makeup_exam_on']);
$orientation = $calendar['orientation_day'];
$midterm = $calendar['midterm_exam_day'];
$installment = $calendar['installment_date'];
$springStart = $calendar['spring_semester_start'];
$configValues = [
'school_year' => $name,
'date_age_reference' => $ageReferenceDate,
'refund_deadline' => $ageReferenceDate,
'enrollment_deadline' => (string) ($schoolYear['registration_ends_on'] ?? ''),
'fall_semester_start' => (string) ($schoolYear['starts_on'] ?? ''),
'last_day_of_school' => (string) ($schoolYear['ends_on'] ?? ''),
'year_start_date' => $yearStart,
'year_end_date' => $yearEnd,
'school_year_start_date' => $yearStart,
'school_year_end_date' => $yearEnd,
'registration_day' => $registrationDay,
'registration_starts_on' => $registrationDay,
'end_of_registration' => $enrollmentDeadline,
'enrollment_deadline' => $enrollmentDeadline,
'1st_day_of_school' => $firstDay,
'first_day_of_school' => $firstDay,
'Installment_date' => $installment,
'installment_date' => $installment,
'fall_semester_start' => $firstDay,
'school_start_date' => $firstDay,
'Due_date' => $firstDay,
'due_date' => $firstDay,
'last_day_of_school' => $lastDay,
'last_school_day' => $lastDay,
'Final_Exam_day' => $finalExam,
'final_exam_day' => $finalExam,
'Make_up_exam' => $makeupExam,
'make_up_exam' => $makeupExam,
'makeup_exam_day' => $makeupExam,
'Orientation_day' => $orientation,
'orientation_day' => $orientation,
'Midterm_exam_day' => $midterm,
'midterm_exam_day' => $midterm,
'spring_semester_start' => $springStart,
];
foreach ($configValues as $key => $value) {
@@ -306,6 +416,76 @@ final class SchoolYearManagementService
}
}
private function withCalendarDefaults(array $payload, string $schoolYearName): array
{
$calendar = $this->calendarPayloadForSchoolYear($schoolYearName);
foreach ($calendar as $key => $value) {
if (! array_key_exists($key, $payload) || trim((string) $payload[$key]) === '') {
$payload[$key] = $value;
}
}
return $payload;
}
private function calendarPayloadForSchoolYear(string $schoolYearName): array
{
if (! preg_match('/^(\d{4})-(\d{4})$/', $schoolYearName, $matches)) {
throw new RuntimeException('Unable to calculate school-year dates: invalid school year format.');
}
$startYear = (int) $matches[1];
$endYear = (int) $matches[2];
$firstDay = $this->nextToLastSundayOfMonth($startYear, 9);
$finalExam = $this->lastMondayOfMonth($endYear, 5)->modify('-8 days');
$midterm = $this->nthSundayOfMonth($endYear, 1, 3);
return [
'starts_on' => sprintf('%04d-08-01', $startYear),
'ends_on' => sprintf('%04d-07-31', $endYear),
'registration_starts_on' => sprintf('%04d-08-01', $startYear),
'registration_ends_on' => $firstDay->modify('+15 days')->format('Y-m-d'),
'fall_makeup_exam_on' => $firstDay->modify('-1 week')->format('Y-m-d'),
'orientation_day' => $firstDay->modify('-2 weeks')->format('Y-m-d'),
'first_day_of_school' => $firstDay->format('Y-m-d'),
'1st_day_of_school' => $firstDay->format('Y-m-d'),
'installment_date' => sprintf('%04d-03-01', $endYear),
'last_day_of_school' => $finalExam->modify('+2 weeks')->format('Y-m-d'),
'last_school_day' => $finalExam->modify('+2 weeks')->format('Y-m-d'),
'final_exam_day' => $finalExam->format('Y-m-d'),
'midterm_exam_day' => $midterm->format('Y-m-d'),
'spring_semester_start' => $midterm->modify('+1 week')->format('Y-m-d'),
];
}
private function nextToLastSundayOfMonth(int $year, int $month): DateTimeImmutable
{
$lastDay = (new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month)))
->modify('last day of this month');
$lastSunday = $lastDay->modify('-' . ((int) $lastDay->format('w')) . ' days');
return $lastSunday->modify('-1 week');
}
private function nthSundayOfMonth(int $year, int $month, int $nth): DateTimeImmutable
{
$firstDay = new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month));
$daysUntilSunday = (7 - (int) $firstDay->format('w')) % 7;
return $firstDay
->modify('+' . $daysUntilSunday . ' days')
->modify('+' . max(0, $nth - 1) . ' weeks');
}
private function lastMondayOfMonth(int $year, int $month): DateTimeImmutable
{
$lastDay = (new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month)))
->modify('last day of this month');
$daysSinceMonday = ((int) $lastDay->format('N') + 6) % 7;
return $lastDay->modify('-' . $daysSinceMonday . ' days');
}
private function ageReferenceDateForSchoolYear(string $schoolYearName): string
{
if (! preg_match('/^(\d{4})-\d{4}$/', $schoolYearName, $matches)) {