fix enrollment logic, add financial aid, fix class distribution
Tests / PHPUnit (push) Failing after 1m6s
Tests / PHPUnit (push) Failing after 1m6s
This commit is contained in:
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user