Fix semester context, attendance rosters, and billing workflows
- load global semester helpers consistently and use date-based semester defaults - fix grading and daily attendance duplicate student/section rows - keep attendance violations scoped to the current semester by default - update invoice, refund, discount, payment, and financial aid flows - add configuration cleanup migrations for duplicate calendar/semester keys - refresh parent registration/report-card and print request handling - update related models, services, views, cron notes, and test coverage
This commit is contained in:
@@ -103,7 +103,7 @@ class AttendanceCalculator implements ScoreCalculatorInterface
|
||||
$fallStartCfg = (string)($this->configModel->getConfig('fall_semester_start') ?? '');
|
||||
$fallEndCfg = (string)($this->configModel->getConfig('fall_end_date') ?? '');
|
||||
$springStartCfg = (string)($this->configModel->getConfig('spring_semester_start') ?? '');
|
||||
$springEndCfg = (string)($this->configModel->getConfig('last_school_day') ?? '');
|
||||
$springEndCfg = (string)($this->configModel->getConfig('last_day_of_school') ?? '');
|
||||
|
||||
if ($norm === 'fall') {
|
||||
$start = ($fallStartCfg !== '') ? sprintf('%04d-%s', $y1, date('m-d', strtotime($fallStartCfg))) : "{$y1}-09-01";
|
||||
|
||||
@@ -23,7 +23,7 @@ class FeeCalculationService
|
||||
$schoolYear = $configModel->getConfig('school_year');
|
||||
$refundDeadline = date('Y-m-d', strtotime($configModel->getConfig('refund_deadline')));
|
||||
$weekOfStudy = (float) ($configModel->getConfig('weeks_study') ?? 8);
|
||||
$schoolEndDate = date('Y-m-d', strtotime($configModel->getConfig('last_school_day')));
|
||||
$schoolEndDate = date('Y-m-d', strtotime($configModel->getConfig('last_day_of_school')));
|
||||
$totalPaid = $paymentModel->getTotalPaidByParentId($parentId, $schoolYear);
|
||||
|
||||
if ($totalPaid <= 0) {
|
||||
@@ -55,7 +55,18 @@ class FeeCalculationService
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Combine all students for proper fee tiering
|
||||
usort($withdrawnStudents, function ($a, $b) {
|
||||
$leftDate = strtotime((string)($a['withdrawal_date'] ?? '')) ?: PHP_INT_MAX;
|
||||
$rightDate = strtotime((string)($b['withdrawal_date'] ?? '')) ?: PHP_INT_MAX;
|
||||
|
||||
if ($leftDate !== $rightDate) {
|
||||
return $leftDate <=> $rightDate;
|
||||
}
|
||||
|
||||
return $this->compareGrades($a['grade'], $b['grade']);
|
||||
});
|
||||
|
||||
// Combine all students for proper fee tiering before withdrawal.
|
||||
$allStudents = array_merge($registeredStudents, $withdrawnStudents);
|
||||
|
||||
// Sort all students by grade for correct tiering
|
||||
@@ -67,17 +78,16 @@ class FeeCalculationService
|
||||
$firstStudentFee = (float) ($configModel->getConfig('first_student_fee') ?? 380);
|
||||
$secondStudentFee = (float) ($configModel->getConfig('second_student_fee') ?? 280);
|
||||
|
||||
// Assign tuition_fee to all students (before filtering refunds)
|
||||
$studentCount = 0;
|
||||
foreach ($allStudents as &$student) {
|
||||
$studentFee = ($studentCount === 0) ? $firstStudentFee : $secondStudentFee;
|
||||
$studentCount++;
|
||||
$student['tuition_fee'] = $studentFee;
|
||||
}
|
||||
unset($student);
|
||||
$refundFeeStack = $this->reverseTuitionRefundFeeStack(
|
||||
count($allStudents),
|
||||
count($registeredStudents),
|
||||
$firstStudentFee,
|
||||
$secondStudentFee
|
||||
);
|
||||
|
||||
// Calculate refund for withdrawn students
|
||||
$refundAmount = 0;
|
||||
$withdrawnRefundIndex = 0;
|
||||
|
||||
foreach ($withdrawnStudents as $student) {
|
||||
if (empty($student['withdrawal_date'])) {
|
||||
@@ -96,7 +106,8 @@ class FeeCalculationService
|
||||
$daysRemaining = $withdrawDateObj->diff($schoolEndDateObj)->days;
|
||||
$weeksRemaining = min($weekOfStudy, max(0, ceil($daysRemaining / 7)));
|
||||
|
||||
$studentFee = (float) ($student['tuition_fee'] ?? 0);
|
||||
$studentFee = (float) ($refundFeeStack[$withdrawnRefundIndex] ?? 0);
|
||||
$withdrawnRefundIndex++;
|
||||
$proportionalRefund = ($studentFee / $weekOfStudy) * $weeksRemaining;
|
||||
$refundAmount += $proportionalRefund;
|
||||
|
||||
@@ -112,6 +123,34 @@ class FeeCalculationService
|
||||
return $refundAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refunds reverse the family tuition stack.
|
||||
*
|
||||
* Example: three students are charged [first, additional, additional].
|
||||
* If one student withdraws, refund the last/additional fee first, not the
|
||||
* withdrawn student's sorted family position.
|
||||
*
|
||||
* @return array<int,float>
|
||||
*/
|
||||
private function reverseTuitionRefundFeeStack(
|
||||
int $originalStudentCount,
|
||||
int $remainingStudentCount,
|
||||
float $firstStudentFee,
|
||||
float $additionalStudentFee
|
||||
): array {
|
||||
$withdrawnCount = max(0, $originalStudentCount - $remainingStudentCount);
|
||||
if ($withdrawnCount === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fees = [];
|
||||
for ($position = $originalStudentCount; $position > $remainingStudentCount; $position--) {
|
||||
$fees[] = $position === 1 ? $firstStudentFee : $additionalStudentFee;
|
||||
}
|
||||
|
||||
return $fees;
|
||||
}
|
||||
|
||||
|
||||
private function compareGrades($gradeA, $gradeB)
|
||||
{
|
||||
|
||||
@@ -147,7 +147,6 @@ final class FinancialAidService
|
||||
$classSectionModel = $this->classSectionModel ?? new ClassSectionModel();
|
||||
$eventChargesModel = $this->eventChargesModel ?? new EventChargesModel();
|
||||
$configurationModel = $this->configurationModel ?? new ConfigurationModel();
|
||||
$userModel = $this->userModel ?? new UserModel();
|
||||
$invoiceIssuanceService = $this->invoiceIssuanceService ?? new InvoiceIssuanceService(
|
||||
$this->requestModel->db,
|
||||
$this->invoiceModel,
|
||||
@@ -155,7 +154,7 @@ final class FinancialAidService
|
||||
$this->invoiceLedgerService
|
||||
);
|
||||
|
||||
$semester = (string) ($configurationModel->getConfig('semester') ?: '');
|
||||
$semester = (string) (getSemester() ?: '');
|
||||
$enrollments = $enrollmentModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
@@ -199,16 +198,12 @@ final class FinancialAidService
|
||||
throw new RuntimeException('Invoice could not be created because this parent has no billable tuition or event charges.');
|
||||
}
|
||||
|
||||
$schoolId = $userModel->getSchoolIdByUserId($parentId);
|
||||
$invoiceNumber = !empty($schoolId)
|
||||
? 'INV-' . $schoolId . '-' . uniqid()
|
||||
: uniqid('INV-');
|
||||
$issueUtc = (new DateTime('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
||||
$dueUtc = $this->invoiceDueUtc($configurationModel);
|
||||
|
||||
$result = $invoiceIssuanceService->issueInvoice(new IssueInvoiceCommand([
|
||||
'parent_id' => $parentId,
|
||||
'invoice_number' => $invoiceNumber,
|
||||
'invoice_number' => $invoiceIssuanceService->generateInvoiceNumber($schoolYear, $parentId),
|
||||
'total_amount' => $totalAmount,
|
||||
'paid_amount' => 0,
|
||||
'balance' => $totalAmount,
|
||||
|
||||
@@ -379,27 +379,20 @@ final class SchoolYearManagementService
|
||||
'school_year' => $name,
|
||||
'date_age_reference' => $ageReferenceDate,
|
||||
'refund_deadline' => $ageReferenceDate,
|
||||
'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,
|
||||
'fall_semester_start' => $yearStart,
|
||||
'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,
|
||||
@@ -448,10 +441,8 @@ final class SchoolYearManagementService
|
||||
'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'),
|
||||
|
||||
@@ -17,7 +17,7 @@ class SemesterRangeService
|
||||
public function getSchoolYearRange(string $schoolYear): array
|
||||
{
|
||||
$startCfg = (string)($this->configModel->getConfig('fall_semester_start') ?? '');
|
||||
$endCfg = (string)($this->configModel->getConfig('last_school_day') ?? '');
|
||||
$endCfg = (string)($this->configModel->getConfig('last_day_of_school') ?? '');
|
||||
|
||||
$start = null;
|
||||
$end = null;
|
||||
@@ -76,7 +76,7 @@ class SemesterRangeService
|
||||
$fallStartCfg = (string)($this->configModel->getConfig('fall_semester_start') ?? '');
|
||||
$fallEndCfg = (string)($this->configModel->getConfig('fall_end_date') ?? '');
|
||||
$springStartCfg = (string)($this->configModel->getConfig('spring_semester_start') ?? '');
|
||||
$springEndCfg = (string)($this->configModel->getConfig('last_school_day') ?? '');
|
||||
$springEndCfg = (string)($this->configModel->getConfig('last_day_of_school') ?? '');
|
||||
|
||||
$md = static fn(string $cfg, int $year, string $fallback): string =>
|
||||
$cfg !== '' ? sprintf('%04d-%s', $year, date('m-d', strtotime($cfg))) : $fallback;
|
||||
@@ -112,8 +112,7 @@ class SemesterRangeService
|
||||
{
|
||||
$fallStartCfg = (string)($this->configModel->getConfig('fall_semester_start') ?? '');
|
||||
$springStartCfg = (string)($this->configModel->getConfig('spring_semester_start') ?? '');
|
||||
$lastDayCfg = (string)($this->configModel->getConfig('last_school_day')
|
||||
?? $this->configModel->getConfig('last_day_of_school') ?? '');
|
||||
$lastDayCfg = (string)($this->configModel->getConfig('last_day_of_school') ?? '');
|
||||
|
||||
try {
|
||||
$target = new DateTimeImmutable($date ?: 'now');
|
||||
|
||||
@@ -32,7 +32,9 @@ class SemesterScoreService
|
||||
$this->semesterScoreModel = $semesterScoreModel;
|
||||
$this->configModel = $configModel;
|
||||
|
||||
$this->semester = (string) $this->configModel->getConfig('semester');
|
||||
require_once APPPATH . 'Helpers/global_config_helper.php';
|
||||
|
||||
$this->semester = (string) \getSemester();
|
||||
$this->schoolYear = (string) $this->configModel->getConfig('school_year');
|
||||
|
||||
// Default actor from session (can be overridden later)
|
||||
|
||||
Reference in New Issue
Block a user