add refund logic and fix books inventory logic
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 47s
Tests / PHPUnit (push) Failing after 1m20s

This commit is contained in:
root
2026-08-22 13:44:25 -04:00
parent 23d1cbb64c
commit d906a915d6
45 changed files with 4711 additions and 442 deletions
+14 -89
View File
@@ -3,7 +3,6 @@
namespace App\Services;
use App\Controllers\View\InvoiceController;
use App\Libraries\RefundEligibilityService;
use App\Models\ClassSectionModel;
use App\Models\EnrollmentModel;
use App\Models\InvoiceModel;
@@ -194,7 +193,6 @@ public function newStudents(string $schoolYear): array
//update enrollment status
public function updateStatuses(?array $enrollmentStatuses, string $schoolYear, string $semester, ?int $performedBy): array
{
$refundService = new FeeCalculationService();
$enrollmentStatusService = \Config\Services::enrollmentStatus(false);
$performedBy = $performedBy ?: ((int) (session()->get('user_id') ?? 0) ?: null);
$this->schoolYear = $schoolYear;
@@ -212,8 +210,8 @@ public function updateStatuses(?array $enrollmentStatuses, string $schoolYear, s
// For batching emails: parent -> status -> [students...]
$groupsByParentStatus = []; // [parent_id][status][] = ['student_id'=>, 'student_name'=>]
$parentInfo = []; // [parent_id] = ['user_id','email','firstname','lastname']
$refundParents = []; // parent_id => true (for refund calc)
$refundAmountByParent = []; // parent_id => amount
$withdrawalPreviewEnrollmentIds = []; // enrollment_id => parent_id
$refundAmountByParent = []; // parent_id => preview amount for notification context
$validStatuses = EnrollmentStatusService::VALID_STATUSES;
@@ -287,7 +285,7 @@ public function updateStatuses(?array $enrollmentStatuses, string $schoolYear, s
];
if ($newEnrollmentStatus === 'refund pending') {
$refundParents[$parentId] = true;
$withdrawalPreviewEnrollmentIds[(int) $result['id']] = $parentId;
}
log_message('info', "Created enrollment for student ID {$studentId} with status {$newEnrollmentStatus} and admission {$admissionStatus}.");
@@ -369,98 +367,25 @@ public function updateStatuses(?array $enrollmentStatuses, string $schoolYear, s
// Mark for refund calc
if ($newEnrollmentStatus === 'refund pending') {
$refundParents[$parentId] = true;
$withdrawalPreviewEnrollmentIds[(int) $enrollmentRow['id']] = (int) $parentId;
}
}
// Compute refunds ONCE per parent needing it
foreach (array_keys($refundParents) as $pid) {
$students = $this->enrollmentModel
->where('parent_id', $pid)
->where('school_year', $this->schoolYear)
->findAll();
if (empty($students)) {
// If a parent is marked for refund but has no enrollments, just log and continue.
log_message('info', "No enrollments found for parent ID {$pid} (for refund calc); skipping refund.");
continue;
}
$invoice = $this->invoiceModel->where('parent_id', $pid)
->where('school_year', $this->schoolYear)
->orderBy('created_at', 'DESC')
->first();
if (!$invoice) {
$errors[] = "No invoice found for parent ID $pid (for refund calc).";
continue;
}
$refundAmount = $refundService->calculateRefund($students, $pid);
$refundAmountByParent[$pid] = $refundAmount;
$existingRefund = $this->refundModel->where('invoice_id', $invoice['id'])->first();
if ($existingRefund) {
$refundId = (int)$existingRefund['id'];
$status = strtolower((string)($existingRefund['status'] ?? ''));
$isApprovedState = in_array($status, ['approved', 'partial', 'paid', 'partially_paid'], true);
$calculatedCents = max(0, (int)round($refundAmount * 100));
$paidCents = (new RefundEligibilityService())->getCompletedPayoutTotalCentsForRefund($refundId);
$targetCents = $isApprovedState ? max($calculatedCents, $paidCents) : $calculatedCents;
$update = [
'refund_amount' => $targetCents / 100,
'updated_by' => session()->get('user_id') ?? null,
];
if ($isApprovedState) {
$update['approved_amount_cents'] = $targetCents;
} else {
$update['status'] = 'Pending';
$update['requested_amount_cents'] = $targetCents;
}
if ($isApprovedState && $paidCents > $calculatedCents) {
$message = sprintf(
'Completed payouts (%0.2f) exceed recalculated refundable credit (%0.2f).',
$paidCents / 100,
$calculatedCents / 100
);
$update['reconciliation_status'] = 'requires_review';
$update['reconciliation_reason'] = $message;
$update['reconciliation_required_at'] = utc_now();
log_message('critical', 'Refund reconciliation required for refund #' . $refundId . ': ' . $message);
} else {
$update['reconciliation_status'] = null;
$update['reconciliation_reason'] = null;
$update['reconciliation_required_at'] = null;
}
$this->refundModel->update($refundId, $update);
} else {
$this->refundModel->insert([
'parent_id' => $pid,
'school_year' => $invoice['school_year'],
'invoice_id' => $invoice['id'],
'refund_amount' => $refundAmount,
'requested_amount_cents' => (int)round($refundAmount * 100),
'approved_amount_cents' => null,
'currency' => 'USD',
'refund_paid_amount' => 0.0,
'status' => 'Pending',
'source_type' => 'tuition_withdrawal',
'source_id' => (int)$invoice['id'],
'requested_at' => utc_now(),
'updated_by' => session()->get('user_id') ?? null,
]);
}
log_message('info', "Refund of $refundAmount created/updated for invoice ID {$invoice['id']} (parent {$pid}).");
}
$this->db->transComplete();
if (!$this->db->transStatus()) {
return ['ok' => false, 'message' => 'A database error occurred. Changes were rolled back.'];
}
foreach ($withdrawalPreviewEnrollmentIds as $enrollmentId => $pid) {
try {
$calculation = service('withdrawalFinancial')->preview((int) $enrollmentId, $performedBy);
$refundAmountByParent[(int) $pid] = ((int) ($calculation['new_refund_request_cents'] ?? 0)) / 100;
} catch (\Throwable $e) {
$errors[] = 'Withdrawal calculation preview failed for enrollment #' . (int) $enrollmentId . ': ' . $e->getMessage();
}
}
// === AFTER COMMIT: fire specific events, batched per parent/status ===
$eventMap = [
'admission under review' => 'admissionUnderReview',
@@ -524,7 +449,7 @@ public function updateStatuses(?array $enrollmentStatuses, string $schoolYear, s
// === Server-side safety net: generate/update invoices for parents whose statuses require it ===
try {
$needsInvoiceFor = ['payment pending', 'enrolled', 'withdrawn', 'refund pending'];
$needsInvoiceFor = ['payment pending', 'enrolled'];
$invCtl = new InvoiceController();
foreach ($groupsByParentStatus as $pid => $byStatus) {
$statuses = array_keys($byStatus);