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:
@@ -64,7 +64,7 @@ class PaymentController extends ResourceController
|
||||
$this->studentModel = new StudentModel();
|
||||
$this->studentClassModel = new StudentClassModel();
|
||||
$this->schoolYear = $this->currentSchoolYearName();
|
||||
$this->semester = $this->configModel->getConfig('semester'); //installment_date
|
||||
$this->semester = getSemester(); //installment_date
|
||||
$this->installmentDate = $this->configModel->getConfig('installment_date');
|
||||
$this->discountUsageModel = new DiscountUsageModel();
|
||||
$this->additionalChargeModel = new AdditionalChargeModel();
|
||||
@@ -151,6 +151,15 @@ class PaymentController extends ResourceController
|
||||
}
|
||||
}
|
||||
|
||||
private function activeSchoolYearName(): string
|
||||
{
|
||||
try {
|
||||
return service('schoolYearContext')->active()->yearName();
|
||||
} catch (\Throwable $e) {
|
||||
return (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
private function assertSchoolYearNameWritable(string $schoolYear): void
|
||||
{
|
||||
service('schoolYearWriteGuard')->assertWritable(
|
||||
@@ -323,6 +332,7 @@ class PaymentController extends ResourceController
|
||||
|
||||
// Read the search term (email or phone)
|
||||
$searchTerm = trim((string) $this->request->getGet('search_term'));
|
||||
$manualPaySchoolYear = $this->activeSchoolYearName();
|
||||
|
||||
// --- Installment end date comes ONLY from config ---
|
||||
$installmentDateRaw = (string) ($this->installmentDate ?? '');
|
||||
@@ -338,29 +348,36 @@ class PaymentController extends ResourceController
|
||||
$searchClean = preg_replace('/\D/', '', $searchTerm);
|
||||
$searchFormatted = $this->formatPhone($searchClean);
|
||||
|
||||
$builder = $this->db->table('users');
|
||||
$builder->select('*');
|
||||
$builder = $this->db->table('users u');
|
||||
$builder->distinct();
|
||||
$builder->select('u.*');
|
||||
$builder->join('user_roles ur', 'ur.user_id = u.id', 'inner');
|
||||
$builder->join('roles r', 'r.id = ur.role_id', 'inner');
|
||||
$builder->where('LOWER(r.name)', 'parent');
|
||||
|
||||
// Build search conditions
|
||||
$builder->groupStart();
|
||||
$builder->where('email', $searchTerm);
|
||||
$builder->where('u.email', $searchTerm);
|
||||
$builder->orLike("CONCAT_WS(' ', u.firstname, u.lastname)", $searchTerm, 'both', null, true);
|
||||
$builder->orLike('u.firstname', $searchTerm);
|
||||
$builder->orLike('u.lastname', $searchTerm);
|
||||
|
||||
if (!empty($searchClean)) {
|
||||
if (strlen($searchClean) === 10) {
|
||||
$formattedPhone = substr($searchClean, 0, 3) . '-' . substr($searchClean, 3, 3) . '-' . substr($searchClean, 6, 4);
|
||||
$builder->orWhere('cellphone', $formattedPhone);
|
||||
$builder->orWhere('u.cellphone', $formattedPhone);
|
||||
}
|
||||
|
||||
// compare stripped formatting
|
||||
$builder->orWhere(
|
||||
"REPLACE(REPLACE(REPLACE(REPLACE(cellphone, '(', ''), ')', ''), '-', ''), ' ', '') =",
|
||||
"REPLACE(REPLACE(REPLACE(REPLACE(u.cellphone, '(', ''), ')', ''), '-', ''), ' ', '') =",
|
||||
$this->db->escapeString($searchClean),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($searchFormatted)) {
|
||||
$builder->orWhere('cellphone', $searchFormatted);
|
||||
$builder->orWhere('u.cellphone', $searchFormatted);
|
||||
}
|
||||
$builder->groupEnd();
|
||||
|
||||
@@ -374,7 +391,7 @@ class PaymentController extends ResourceController
|
||||
if ($parent && !empty($parent['id'])) {
|
||||
$parentData = $parent;
|
||||
$parentId = (int) $parent['id'];
|
||||
$carryForwardPaymentRequired = $this->parentHasActiveCarryForwardBalance($parentId, $this->schoolYear);
|
||||
$carryForwardPaymentRequired = $this->parentHasActiveCarryForwardBalance($parentId, $manualPaySchoolYear);
|
||||
if ($carryForwardPaymentRequired) {
|
||||
$carryForwardPaymentMessage = 'This parent has a balance carried over from a previous school year. Manual payments must be paid in full; installments are not allowed.';
|
||||
}
|
||||
@@ -386,14 +403,14 @@ class PaymentController extends ResourceController
|
||||
|
||||
// Payments (paginated). Join invoices so history is filtered by invoice term
|
||||
// and displays current invoice state instead of stale payment snapshots.
|
||||
$selectedYear = $this->getSelectedPaymentHistoryYear();
|
||||
$paymentHistory = $this->paymentModel->parentPaymentHistoryQuery($parentId, $selectedYear);
|
||||
$paymentHistory = $this->paymentModel->parentPaymentHistoryQuery($parentId, $manualPaySchoolYear);
|
||||
$payments = $paymentHistory->paginate(10);
|
||||
$pager = $paymentHistory->pager;
|
||||
|
||||
// Invoices
|
||||
$rawInvoices = $this->invoiceModel
|
||||
->where('parent_id', $parentId) // <- fixed (removed "value:")
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $manualPaySchoolYear)
|
||||
->orderBy('issue_date', 'DESC')
|
||||
->findAll();
|
||||
|
||||
@@ -460,21 +477,23 @@ class PaymentController extends ResourceController
|
||||
$qs = '%' . $db->escapeLikeString($q) . '%';
|
||||
$digits = preg_replace('/\D/', '', $q);
|
||||
|
||||
$sql = "SELECT id, firstname, lastname, email, cellphone
|
||||
FROM users
|
||||
$sql = "SELECT DISTINCT u.id, u.firstname, u.lastname, u.email, u.cellphone
|
||||
FROM users u
|
||||
JOIN user_roles ur ON ur.user_id = u.id
|
||||
JOIN roles r ON r.id = ur.role_id AND LOWER(r.name) = 'parent'
|
||||
WHERE (
|
||||
CONCAT_WS(' ', firstname, lastname) LIKE ?
|
||||
OR email LIKE ?
|
||||
OR cellphone LIKE ?";
|
||||
CONCAT_WS(' ', u.firstname, u.lastname) LIKE ?
|
||||
OR u.email LIKE ?
|
||||
OR u.cellphone LIKE ?";
|
||||
$params = [$qs, $qs, $qs];
|
||||
|
||||
if ($digits !== '') {
|
||||
$sql .= " OR REPLACE(REPLACE(REPLACE(REPLACE(cellphone, '(', ''), ')', ''), '-', ''), ' ', '') LIKE ?";
|
||||
$sql .= " OR REPLACE(REPLACE(REPLACE(REPLACE(u.cellphone, '(', ''), ')', ''), '-', ''), ' ', '') LIKE ?";
|
||||
$params[] = '%' . $digits . '%';
|
||||
}
|
||||
|
||||
$sql .= ")
|
||||
ORDER BY lastname, firstname
|
||||
ORDER BY u.lastname, u.firstname
|
||||
LIMIT 20";
|
||||
|
||||
$rows = $db->query($sql, $params)->getResultArray();
|
||||
@@ -562,7 +581,7 @@ class PaymentController extends ResourceController
|
||||
->orWhere('enrollment_status', 'Payment pending')
|
||||
->orWhere('enrollment_status', 'Payment Pending')
|
||||
->orWhere('enrollment_status', 'PAYMENT PENDING')
|
||||
->orWhere("LOWER(TRIM(REPLACE(enrollment_status, CHAR(160), ' '))) = 'payment pending'", null, false)
|
||||
->orWhere("LOWER(TRIM(REPLACE(enrollment_status, CONVERT(0xC2A0 USING utf8mb4), ' '))) = 'payment pending'", null, false)
|
||||
->groupEnd();
|
||||
|
||||
if ($semester !== null) {
|
||||
@@ -1457,12 +1476,15 @@ class PaymentController extends ResourceController
|
||||
if ($amount > $preBalance + 0.00001) {
|
||||
return false;
|
||||
}
|
||||
$postBalance = max(0.0, round($preBalance - $amount, 2));
|
||||
$paymentData = [
|
||||
'parent_id' => (int) $invoice['parent_id'],
|
||||
'invoice_id' => $invoiceId,
|
||||
'total_amount' => $invoice['total_amount'],
|
||||
'paid_amount' => $amount,
|
||||
'balance' => null,
|
||||
'balance' => $postBalance,
|
||||
'balance_amount' => $postBalance,
|
||||
'balance_after_payment' => $postBalance,
|
||||
'number_of_installments' => $installmentSeq, // <-- installment sequence (1,2,3,...)
|
||||
'installment_seq' => $installmentSeq,
|
||||
'transaction_id' => $transactionId,
|
||||
@@ -1480,6 +1502,7 @@ class PaymentController extends ResourceController
|
||||
|
||||
$paymentId = $this->paymentModel->insert($paymentData);
|
||||
if (!$paymentId) {
|
||||
log_message('error', '[processPayment] Failed to insert payment: ' . json_encode($this->paymentModel->errors()));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user