This commit is contained in:
@@ -81,7 +81,8 @@ class InvoiceController extends ResourceController
|
||||
$this->gradeFee = $this->configModel->getConfig('grade_fee');
|
||||
$this->schoolYear = $this->currentSchoolYearName();
|
||||
$this->semester = $this->configModel->getConfig('semester');
|
||||
$this->dueDate = $this->configModel->getConfig('due_date');
|
||||
$this->dueDate = $this->configModel->getConfig('first_day_of_school')
|
||||
?: $this->configModel->getConfig('due_date');
|
||||
$this->firstStudentFee = (float) ($this->configModel->getConfig('first_student_fee') ?? 350);
|
||||
$this->secondStudentFee = (float) ($this->configModel->getConfig('second_student_fee') ?? 200);
|
||||
$this->youthFee = (float) ($this->configModel->getConfig('youth_fee') ?? 200);
|
||||
@@ -230,6 +231,13 @@ class InvoiceController extends ResourceController
|
||||
}
|
||||
}
|
||||
|
||||
private function assertSchoolYearNameWritable(string $schoolYear): void
|
||||
{
|
||||
service('schoolYearWriteGuard')->assertWritable(
|
||||
service('schoolYearContext')->forYearName($schoolYear)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* API: Invoice management composite data (used by invoice_management view)
|
||||
* Returns the same structure previously rendered server-side in index().
|
||||
@@ -891,7 +899,7 @@ class InvoiceController extends ResourceController
|
||||
}
|
||||
}
|
||||
|
||||
$eventsList = $this->chargesModel->getChargesWithEventInfo($parentId, $schoolYear);
|
||||
$eventsList = $this->chargesModel->getChargesWithEventInfo($parentId, $schoolYear, $invoice['semester'] ?? null);
|
||||
|
||||
// Attach SCHOOL IDs
|
||||
$allKids = array_merge($registeredKids, $withdrawnKids);
|
||||
@@ -1073,18 +1081,40 @@ class InvoiceController extends ResourceController
|
||||
$pdf->SetFont('Arial', 'B', 12);
|
||||
$pdf->Cell(40, 6, 'Due Date:', 0, 0, 'L');
|
||||
$pdf->SetFont('Arial', '', 12);
|
||||
$dueLocal = null;
|
||||
$formatCalendarDate = static function ($raw): ?string {
|
||||
if ($raw instanceof \DateTimeInterface) {
|
||||
return $raw->format('m-d-Y');
|
||||
}
|
||||
|
||||
$value = trim((string)($raw ?? ''));
|
||||
if ($value === '' || preg_match('/^0{4}-0{2}-0{2}/', $value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $value, $matches)) {
|
||||
return $matches[2] . '-' . $matches[3] . '-' . $matches[1];
|
||||
}
|
||||
|
||||
if (preg_match('/^(\d{1,2})[-\/](\d{1,2})[-\/](\d{4})/', $value, $matches)) {
|
||||
return sprintf('%02d-%02d-%04d', (int)$matches[1], (int)$matches[2], (int)$matches[3]);
|
||||
}
|
||||
|
||||
$timestamp = strtotime($value);
|
||||
return $timestamp === false ? null : date('m-d-Y', $timestamp);
|
||||
};
|
||||
|
||||
$dueDisplay = $formatCalendarDate($invoice['due_date'] ?? null);
|
||||
try {
|
||||
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
|
||||
if (!empty($invoice['due_date'])) {
|
||||
$dueLocal = (new \DateTimeImmutable($invoice['due_date'], new \DateTimeZone('UTC')))
|
||||
->setTimezone(new \DateTimeZone($tzName));
|
||||
} elseif (!empty($invoice['created_at'])) {
|
||||
if ($dueDisplay === null && !empty($invoice['created_at'])) {
|
||||
$dueLocal = new \DateTimeImmutable($invoice['created_at'], new \DateTimeZone($tzName));
|
||||
$dueDisplay = $dueLocal->format('m-d-Y');
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
if (!$dueLocal) { $dueLocal = new \DateTimeImmutable('now', new \DateTimeZone($tzName ?? 'UTC')); }
|
||||
$pdf->Cell(0, 6, $dueLocal->format('m-d-Y'), 0, 1, 'L');
|
||||
if ($dueDisplay === null) {
|
||||
$dueDisplay = (new \DateTimeImmutable('now', new \DateTimeZone($tzName ?? 'UTC')))->format('m-d-Y');
|
||||
}
|
||||
$pdf->Cell(0, 6, $dueDisplay, 0, 1, 'L');
|
||||
|
||||
$pdf->Ln(5);
|
||||
$pdf->SetFont('Arial', '', 9);
|
||||
@@ -1145,16 +1175,111 @@ class InvoiceController extends ResourceController
|
||||
];
|
||||
};
|
||||
|
||||
// --- Frozen invoice charge lines. Do not rebuild issued charges from current enrollment/events.
|
||||
$studentById = [];
|
||||
foreach (($data['students'] ?? []) as $student) {
|
||||
$sid = (int)($student['student_id'] ?? 0);
|
||||
if ($sid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$studentById[$sid] = trim((string)($student['student_firstname'] ?? '') . ' ' . (string)($student['student_lastname'] ?? ''));
|
||||
}
|
||||
|
||||
$studentTuitionRows = [];
|
||||
foreach (array_merge($registeredKids ?? [], $withdrawnKids ?? []) as $student) {
|
||||
$sid = (int)($student['student_id'] ?? 0);
|
||||
$charge = $studentCharges[$sid] ?? null;
|
||||
$amount = (float)($charge['unit_fee'] ?? 0.0);
|
||||
if ($sid <= 0 || abs($amount) < 0.00001) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = trim((string)($student['student_firstname'] ?? '') . ' ' . (string)($student['student_lastname'] ?? ''));
|
||||
$grade = trim((string)($student['grade'] ?? ''));
|
||||
$desc = 'Tuition - ' . ($name !== '' ? $name : 'Student #' . $sid);
|
||||
if ($grade !== '' && strtoupper($grade) !== 'N/A') {
|
||||
$desc .= ' (' . $grade . ')';
|
||||
}
|
||||
|
||||
$studentTuitionRows[] = [
|
||||
'description' => $desc,
|
||||
'amount' => $amount,
|
||||
];
|
||||
}
|
||||
|
||||
$eventRows = [];
|
||||
foreach (($events ?? []) as $event) {
|
||||
$amount = (float)($event['charged'] ?? 0.0);
|
||||
if (abs($amount) < 0.00001) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sid = (int)($event['student_id'] ?? 0);
|
||||
$studentName = $sid > 0 ? ($studentById[$sid] ?? '') : '';
|
||||
if ($studentName === '') {
|
||||
$studentName = trim((string)($event['external_firstname'] ?? '') . ' ' . (string)($event['external_lastname'] ?? ''));
|
||||
}
|
||||
|
||||
$desc = trim((string)($event['event_name'] ?? 'Event charge'));
|
||||
if ($studentName !== '') {
|
||||
$desc .= ' - ' . $studentName;
|
||||
}
|
||||
|
||||
$eventRows[] = [
|
||||
'description' => $desc,
|
||||
'amount' => $amount,
|
||||
];
|
||||
}
|
||||
|
||||
// --- Frozen invoice charge lines remain authoritative for totals.
|
||||
// Aggregate tuition/event lines are expanded for display when invoice details are available.
|
||||
foreach (($data['invoiceLines'] ?? []) as $line) {
|
||||
$dt = $toLocal($line['created_at'] ?? ($invoice['created_at'] ?? null), true);
|
||||
$amount = ((int)($line['line_amount_cents'] ?? 0)) / 100;
|
||||
$type = (string)($line['line_type'] ?? 'other');
|
||||
$category = str_contains($type, 'event') ? 'event'
|
||||
: (str_contains($type, 'additional') ? 'additional' : 'registration');
|
||||
|
||||
if (!str_contains($type, 'additional') && !str_contains($type, 'event') && !empty($studentTuitionRows)) {
|
||||
$expandedTotal = 0.0;
|
||||
foreach ($studentTuitionRows as $row) {
|
||||
$expandedTotal += (float)$row['amount'];
|
||||
$push($dt, $row['description'], (float)$row['amount'], 'registration');
|
||||
}
|
||||
|
||||
$delta = round($amount - $expandedTotal, 2);
|
||||
if (abs($delta) >= 0.01) {
|
||||
$push($dt, 'Tuition charges adjustment', $delta, 'registration');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_contains($type, 'event') && !empty($eventRows)) {
|
||||
$expandedTotal = 0.0;
|
||||
foreach ($eventRows as $row) {
|
||||
$expandedTotal += (float)$row['amount'];
|
||||
$push($dt, $row['description'], (float)$row['amount'], 'event');
|
||||
}
|
||||
|
||||
$delta = round($amount - $expandedTotal, 2);
|
||||
if (abs($delta) >= 0.01) {
|
||||
$push($dt, 'Event charges adjustment', $delta, 'event');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$push($dt, (string)($line['description'] ?? 'Invoice line'), $amount, $category);
|
||||
}
|
||||
|
||||
if (empty($data['invoiceLines'] ?? [])) {
|
||||
$fallbackDt = $toLocal($invoice['created_at'] ?? ($invoice['issue_date'] ?? null), true);
|
||||
foreach ($studentTuitionRows as $row) {
|
||||
$push($fallbackDt, $row['description'], (float)$row['amount'], 'registration');
|
||||
}
|
||||
foreach ($eventRows as $row) {
|
||||
$push($fallbackDt, $row['description'], (float)$row['amount'], 'event');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Payments (negative) — stored in local time
|
||||
foreach ($payments as $payment) {
|
||||
$dt = $toLocal($payment['payment_date'] ?? null, false /* local */);
|
||||
@@ -1513,6 +1638,12 @@ private function getGradeLevel($grade): array
|
||||
// API: Update invoice status
|
||||
public function updateStatusAPI($invoiceId)
|
||||
{
|
||||
$invoice = $this->invoiceModel->find($invoiceId);
|
||||
if (!$invoice) {
|
||||
return $this->failNotFound('Invoice not found.');
|
||||
}
|
||||
$this->assertSchoolYearNameWritable((string)($invoice['school_year'] ?? ''));
|
||||
|
||||
$status = $this->request->getPost('status');
|
||||
if ($this->invoiceModel->updateInvoiceStatus($invoiceId, $status)) {
|
||||
return $this->respond(['status' => 'success']);
|
||||
@@ -1524,6 +1655,12 @@ private function getGradeLevel($grade): array
|
||||
// View: Update invoice status
|
||||
public function updateStatus($invoiceId)
|
||||
{
|
||||
$invoice = $this->invoiceModel->find($invoiceId);
|
||||
if (!$invoice) {
|
||||
return redirect()->back()->with('error', 'Invoice not found.');
|
||||
}
|
||||
$this->assertSchoolYearNameWritable((string)($invoice['school_year'] ?? ''));
|
||||
|
||||
$status = $this->request->getPost('status');
|
||||
if ($this->invoiceModel->updateInvoiceStatus($invoiceId, $status)) {
|
||||
return redirect()->to('/invoices');
|
||||
|
||||
Reference in New Issue
Block a user