fix installment for carry over balance parent account
This commit is contained in:
@@ -235,6 +235,13 @@ class EnrollmentAdminController extends BaseController
|
||||
}
|
||||
|
||||
$ruleCodes = $this->ruleCodesForFlag((string) $flag['flag_type']);
|
||||
if (
|
||||
(string) $flag['flag_type'] === 'FINANCIAL_REVIEW_REQUIRED'
|
||||
&& (string) ($this->request->getPost('allow_current_year_installments') ?? '') === '1'
|
||||
) {
|
||||
$ruleCodes[] = 'CURRENT_YEAR_INSTALLMENT_OVERRIDE';
|
||||
$ruleCodes = array_values(array_unique($ruleCodes));
|
||||
}
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
$this->db->transStart();
|
||||
@@ -349,7 +356,9 @@ class EnrollmentAdminController extends BaseController
|
||||
$postedCodes = is_array($postedCodes) ? array_values(array_filter(array_map('strval', $postedCodes))) : [];
|
||||
$postedCodes = array_values(array_unique(array_map(static fn (string $code): string => strtoupper(trim($code)), $postedCodes)));
|
||||
$failedCodes = array_values(array_unique(array_map(static fn (string $code): string => strtoupper(trim($code)), $failedCodes)));
|
||||
$ruleCodes = $postedCodes !== [] ? array_values(array_intersect($postedCodes, $failedCodes)) : $failedCodes;
|
||||
$allowedCodes = $failedCodes;
|
||||
$allowedCodes[] = 'CURRENT_YEAR_INSTALLMENT_OVERRIDE';
|
||||
$ruleCodes = $postedCodes !== [] ? array_values(array_intersect($postedCodes, $allowedCodes)) : $failedCodes;
|
||||
$ruleCodes = array_values(array_filter($ruleCodes, static fn (string $code): bool => $code !== ''));
|
||||
$nonOverridable = array_values(array_intersect($ruleCodes, ['ADULT_STUDENT_PARENT_BLOCKED']));
|
||||
if ($nonOverridable !== []) {
|
||||
|
||||
@@ -287,6 +287,15 @@ class ParentController extends BaseController
|
||||
$previousSchoolYear = $this->previousSchoolYearName($selectedYear);
|
||||
$fallMakeupExamOn = $this->fallMakeupExamDateForYear($selectedYear);
|
||||
|
||||
if ($previousSchoolYear !== null) {
|
||||
service('enrollmentTransition')->syncParentFinancialReviewFlags(
|
||||
(int) $parentId,
|
||||
$previousSchoolYear,
|
||||
$selectedYear,
|
||||
array_values(array_map(static fn (array $student): int => (int) ($student['id'] ?? 0), $students))
|
||||
);
|
||||
}
|
||||
|
||||
// Map enrollment statuses
|
||||
$statusMap = [
|
||||
'admission under review' => 'admission under review',
|
||||
@@ -404,15 +413,6 @@ class ParentController extends BaseController
|
||||
)));
|
||||
}
|
||||
|
||||
if ($previousSchoolYear !== null) {
|
||||
service('enrollmentTransition')->syncParentFinancialReviewFlags(
|
||||
(int) $parentId,
|
||||
$previousSchoolYear,
|
||||
$selectedYear,
|
||||
array_values(array_map(static fn (array $student): int => (int) ($student['id'] ?? 0), $students))
|
||||
);
|
||||
}
|
||||
|
||||
// Render view
|
||||
return view('/parent/enroll_classes', [
|
||||
'students' => $students,
|
||||
|
||||
@@ -391,9 +391,14 @@ class PaymentController extends ResourceController
|
||||
if ($parent && !empty($parent['id'])) {
|
||||
$parentData = $parent;
|
||||
$parentId = (int) $parent['id'];
|
||||
$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.';
|
||||
$hasCarryForwardInvoice = $this->parentHasCarryForwardInvoice($parentId, $manualPaySchoolYear);
|
||||
$hasCurrentYearInstallmentOverride = $this->parentHasCurrentYearInstallmentOverride($parentId, $manualPaySchoolYear);
|
||||
$carryForwardPaymentRequired = $hasCarryForwardInvoice && ! $hasCurrentYearInstallmentOverride;
|
||||
if ($hasCarryForwardInvoice) {
|
||||
$carryForwardPaymentMessage = 'This parent has a carry-over balance record from a previous school year. Installments are not allowed without an admin exception.';
|
||||
if ($hasCurrentYearInstallmentOverride) {
|
||||
$carryForwardPaymentMessage = 'Carry-over invoices must be paid in full. Installments are allowed only for current-year balances by admin override.';
|
||||
}
|
||||
}
|
||||
|
||||
// Students
|
||||
@@ -433,6 +438,7 @@ class PaymentController extends ResourceController
|
||||
|
||||
// Always use the configured end date for installments
|
||||
$inv['due_ymd'] = $installmentEndYmd;
|
||||
$inv['is_carry_forward_invoice'] = $this->isCarryForwardInvoiceRow($inv) ? 1 : 0;
|
||||
|
||||
// Optional: keep a start marker if you ever need it elsewhere
|
||||
$issueYmd = '';
|
||||
@@ -956,7 +962,7 @@ class PaymentController extends ResourceController
|
||||
try {
|
||||
// Lock invoice & get context (also ensure totals are up-to-date before validation)
|
||||
$row = $this->db->query(
|
||||
'SELECT id, parent_id, invoice_number, total_amount, school_year FROM invoices WHERE id = ? FOR UPDATE',
|
||||
'SELECT id, parent_id, invoice_number, total_amount, school_year, semester, description FROM invoices WHERE id = ? FOR UPDATE',
|
||||
[$invoiceId]
|
||||
)->getRowArray();
|
||||
|
||||
@@ -972,7 +978,10 @@ class PaymentController extends ResourceController
|
||||
// Recompute invoice totals from tuition + events + additional charges
|
||||
$currentBalance = (float) $this->invoiceLedgerService->recalculateInvoice($invoiceId)['balance'];
|
||||
|
||||
$carryForwardPaymentRequired = $this->parentHasActiveCarryForwardBalance($parentId, $invYear);
|
||||
$hasCarryForwardInvoice = $this->parentHasCarryForwardInvoice($parentId, $invYear);
|
||||
$hasCurrentYearInstallmentOverride = $this->parentHasCurrentYearInstallmentOverride($parentId, $invYear);
|
||||
$carryForwardPaymentRequired = $hasCarryForwardInvoice
|
||||
&& ($this->isCarryForwardInvoiceRow($row) || ! $hasCurrentYearInstallmentOverride);
|
||||
if ($carryForwardPaymentRequired && $paymentType === 'installment') {
|
||||
$this->db->transRollback();
|
||||
$this->financialAttachmentService->discardStagedFile($stagedEvidence);
|
||||
@@ -1061,6 +1070,8 @@ class PaymentController extends ResourceController
|
||||
// Post-payment balance from snapshot
|
||||
$postBalance = (float) ($ledger['balance'] ?? max(0.0, round($initialPreBalance - $amount, 2)));
|
||||
|
||||
$this->syncEnrollmentFinanceAfterPayment($parentId, $invYear);
|
||||
|
||||
// Optional enrollment update
|
||||
$enrollmentupdated = $this->updateEnrollmentStatusIfPaid($invoiceId);
|
||||
if ($enrollmentupdated != 0) {
|
||||
@@ -1268,15 +1279,14 @@ class PaymentController extends ResourceController
|
||||
}
|
||||
}
|
||||
|
||||
private function parentHasActiveCarryForwardBalance(int $parentId, ?string $schoolYear = null): bool
|
||||
private function parentHasCarryForwardInvoice(int $parentId, ?string $schoolYear = null): bool
|
||||
{
|
||||
if ($parentId <= 0 || ! $this->db->tableExists('invoices')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$builder = $this->db->table('invoices')
|
||||
->where('parent_id', $parentId)
|
||||
->where('balance >', 0);
|
||||
->where('parent_id', $parentId);
|
||||
|
||||
if ($schoolYear !== null && $schoolYear !== '') {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
@@ -1290,6 +1300,7 @@ class PaymentController extends ResourceController
|
||||
$builder
|
||||
->orLike('description', 'carried over')
|
||||
->orLike('description', 'carry-forward')
|
||||
->orLike('description', 'carry over')
|
||||
->orLike('description', 'previous school year');
|
||||
}
|
||||
|
||||
@@ -1298,6 +1309,85 @@ class PaymentController extends ResourceController
|
||||
return $builder->countAllResults() > 0;
|
||||
}
|
||||
|
||||
private function parentHasCurrentYearInstallmentOverride(int $parentId, string $schoolYear): bool
|
||||
{
|
||||
if ($parentId <= 0 || $schoolYear === '' || ! $this->db->tableExists('enrollment_exceptions')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$rows = $this->db->table('enrollment_exceptions')
|
||||
->select('bypassed_rule_codes_json')
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('status', ['active', 'used'])
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$codes = json_decode((string) ($row['bypassed_rule_codes_json'] ?? ''), true);
|
||||
if (! is_array($codes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$codes = array_map(static fn ($code): string => strtoupper(trim((string) $code)), $codes);
|
||||
if (in_array('CURRENT_YEAR_INSTALLMENT_OVERRIDE', $codes, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isCarryForwardInvoiceRow(array $invoice): bool
|
||||
{
|
||||
$invoiceNumber = (string) ($invoice['invoice_number'] ?? '');
|
||||
if (str_starts_with($invoiceNumber, 'CF-')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcasecmp((string) ($invoice['semester'] ?? ''), 'Opening Balance') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$description = strtolower((string) ($invoice['description'] ?? ''));
|
||||
|
||||
return str_contains($description, 'carried over')
|
||||
|| str_contains($description, 'carry-forward')
|
||||
|| str_contains($description, 'carry over')
|
||||
|| str_contains($description, 'previous school year');
|
||||
}
|
||||
|
||||
private function syncEnrollmentFinanceAfterPayment(int $parentId, string $targetSchoolYear): void
|
||||
{
|
||||
$sourceSchoolYear = $this->previousSchoolYearName($targetSchoolYear);
|
||||
if ($parentId <= 0 || $sourceSchoolYear === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$studentIds = $this->studentModel->getStudentIdsByParentId($parentId);
|
||||
service('enrollmentTransition')->syncParentFinancialReviewFlags(
|
||||
$parentId,
|
||||
$sourceSchoolYear,
|
||||
$targetSchoolYear,
|
||||
array_values(array_map('intval', $studentIds ?? []))
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Enrollment finance sync after payment failed for parent {parent_id}, school year {school_year}: {error}', [
|
||||
'parent_id' => $parentId,
|
||||
'school_year' => $targetSchoolYear,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function previousSchoolYearName(string $schoolYear): ?string
|
||||
{
|
||||
return preg_match('/^(\d{4})-(\d{4})$/', trim($schoolYear), $matches)
|
||||
? ((int) $matches[1] - 1) . '-' . ((int) $matches[2] - 1)
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 🔄 Helper: Recalculate invoice totals and status based on all payments for current school year
|
||||
|
||||
@@ -651,6 +651,13 @@ class WhatsappController extends BaseController
|
||||
// ignore membership annotation errors
|
||||
}
|
||||
|
||||
// Annotate latest invite delivery state from whatsapp_invites_log.
|
||||
try {
|
||||
$this->annotateWhatsappDeliveryStatuses($classSections, $schoolYear);
|
||||
} catch (\Throwable $e) {
|
||||
// Keep the roster usable even if delivery history cannot be loaded.
|
||||
}
|
||||
|
||||
// 4) (Optional) Teachers, if you keep them in teacher_class
|
||||
try {
|
||||
$tb = $this->db->table('teacher_class tc')
|
||||
@@ -687,6 +694,83 @@ class WhatsappController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add latest email delivery state for each primary parent/class row.
|
||||
*
|
||||
* Exact class-section logs win. Older consolidated "all classes" logs without
|
||||
* class_section_id are used only as a fallback for the parent/year.
|
||||
*/
|
||||
private function annotateWhatsappDeliveryStatuses(array &$classSections, string $schoolYear): void
|
||||
{
|
||||
if (! $this->db->tableExists('whatsapp_invites_log') || empty($classSections)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sectionIds = array_keys($classSections);
|
||||
$parentIds = [];
|
||||
foreach ($classSections as $sec) {
|
||||
foreach (($sec['parents'] ?? []) as $p) {
|
||||
$primaryId = (int) ($p['primary_id'] ?? 0);
|
||||
if ($primaryId > 0) {
|
||||
$parentIds[$primaryId] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($sectionIds) || empty($parentIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = $this->db->table('whatsapp_invites_log')
|
||||
->select('parent_id, class_section_id, status, error_message, sent_at')
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('parent_id', array_keys($parentIds))
|
||||
->groupStart()
|
||||
->whereIn('class_section_id', $sectionIds)
|
||||
->orWhere('class_section_id IS NULL', null, false)
|
||||
->groupEnd()
|
||||
->orderBy('sent_at', 'DESC')
|
||||
->orderBy('id', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$exact = [];
|
||||
$fallbackByParent = [];
|
||||
foreach ($rows as $row) {
|
||||
$parentId = (int) ($row['parent_id'] ?? 0);
|
||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
if ($parentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($sectionId > 0) {
|
||||
$key = $sectionId . ':' . $parentId;
|
||||
if (! isset($exact[$key])) {
|
||||
$exact[$key] = $row;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! isset($fallbackByParent[$parentId])) {
|
||||
$fallbackByParent[$parentId] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($classSections as $sid => &$sec) {
|
||||
foreach ($sec['parents'] as &$p) {
|
||||
$primaryId = (int) ($p['primary_id'] ?? 0);
|
||||
$log = $exact[$sid . ':' . $primaryId] ?? $fallbackByParent[$primaryId] ?? null;
|
||||
|
||||
$p['delivery_status'] = $log ? strtolower((string) ($log['status'] ?? '')) : 'not_sent';
|
||||
$p['delivery_sent_at'] = $log['sent_at'] ?? null;
|
||||
$p['delivery_error'] = $log['error_message'] ?? null;
|
||||
$p['delivery_class_specific'] = $log && ! empty($log['class_section_id']);
|
||||
}
|
||||
unset($p);
|
||||
}
|
||||
unset($sec);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: Update WhatsApp group membership flags for a class/parent(s).
|
||||
* Accepts fields:
|
||||
|
||||
Reference in New Issue
Block a user