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
+114 -13
View File
@@ -16,7 +16,7 @@ use App\Models\PaymentModel;
use App\Models\ConfigurationModel;
use App\Models\InvoiceModel;
use App\Models\EnrollmentModel;
use App\Services\FeeCalculationService;
use App\Services\EnrollmentStatusService;
use CodeIgniter\Exceptions\PageNotFoundException;
class RefundController extends BaseController
@@ -32,7 +32,6 @@ class RefundController extends BaseController
protected ParentLedgerService $parentLedgerService;
protected RefundEligibilityService $refundEligibilityService;
protected FinancialAttachmentService $financialAttachmentService;
protected FeeCalculationService $feeCalculationService;
protected $db;
// Allowed request types (mapped to your `refunds.request` column)
@@ -54,7 +53,6 @@ class RefundController extends BaseController
$this->parentLedgerService = new ParentLedgerService();
$this->refundEligibilityService = new RefundEligibilityService();
$this->financialAttachmentService = new FinancialAttachmentService();
$this->feeCalculationService = new FeeCalculationService();
$this->db = \Config\Database::connect();
}
@@ -723,6 +721,10 @@ class RefundController extends BaseController
throw new FinancialPersistenceException('REFUND_PROJECTION_UPDATE_FAILED', $this->refundModel->errors());
}
if (!$isOnline && $newStatus === FinancialStatus::REFUND_PAID) {
$this->markWithdrawalEnrollmentComplete($lockedRefund);
}
if (!$isOnline && $affectedInvoiceId > 0) {
$this->invoiceLedgerService->recalculateInvoice($affectedInvoiceId);
}
@@ -774,6 +776,49 @@ class RefundController extends BaseController
return $this->response->setJSON($payload);
}
private function markWithdrawalEnrollmentComplete(array $refund): void
{
if ((string)($refund['source_type'] ?? '') !== 'tuition_withdrawal') {
return;
}
$calculationId = (int)($refund['withdrawal_calculation_id'] ?? 0);
if ($calculationId <= 0 || ! $this->db->tableExists('withdrawal_financial_calculations')) {
return;
}
$calculation = $this->db->query(
'SELECT enrollment_id FROM withdrawal_financial_calculations WHERE id = ? FOR UPDATE',
[$calculationId]
)->getRowArray();
$enrollmentId = (int)($calculation['enrollment_id'] ?? 0);
if ($enrollmentId <= 0) {
return;
}
$enrollment = $this->db->query(
'SELECT * FROM enrollments WHERE id = ? FOR UPDATE',
[$enrollmentId]
)->getRowArray();
if (! $enrollment) {
return;
}
$statusService = new EnrollmentStatusService($this->db);
$statusService->upsertStatus([
'id' => (int)$enrollment['id'],
'student_id' => (int)$enrollment['student_id'],
'parent_id' => (int)$enrollment['parent_id'],
'school_year' => (string)$enrollment['school_year'],
'semester' => (string)($enrollment['semester'] ?? ''),
'enrollment_status' => 'withdrawn',
'admission_status' => (string)($enrollment['admission_status'] ?? 'pending'),
'is_withdrawn' => 1,
'withdrawal_date' => $enrollment['withdrawal_date'] ?? null,
'updated_at' => utc_now(),
], (int)(session()->get('user_id') ?? 0) ?: null, 'withdrawal_refund_paid');
}
public function reversePayout(?int $routePayoutId = null)
{
$payoutId = (int)($routePayoutId ?: $this->request->getPost('payout_id'));
@@ -933,6 +978,7 @@ class RefundController extends BaseController
{
// Repair legacy withdrawal placeholders only; overpayment recalculation remains explicit.
$this->repairPendingWithdrawalRefunds();
$withdrawalReviews = $this->pendingWithdrawalReviews();
// 2) List refunds with joins
$refunds = $this->refundModel
@@ -987,10 +1033,41 @@ class RefundController extends BaseController
return view('refunds/list', [
'refunds' => $refunds,
'parentId' => $parentId
'parentId' => $parentId,
'withdrawalReviews' => $withdrawalReviews,
]);
}
/** @return list<array<string,mixed>> */
private function pendingWithdrawalReviews(): array
{
try {
if (! $this->db->tableExists('withdrawal_financial_calculations')) {
return [];
}
return $this->db->table('withdrawal_financial_calculations wfc')
->select('wfc.*,
i.invoice_number,
u.firstname AS parent_firstname,
u.lastname AS parent_lastname,
s.firstname AS student_firstname,
s.lastname AS student_lastname')
->join('invoices i', 'wfc.invoice_id = i.id', 'left')
->join('users u', 'wfc.parent_id = u.id', 'left')
->join('students s', 'wfc.student_id = s.id', 'left')
->whereIn('wfc.status', ['preview', 'requires_review'])
->where('wfc.posted_at', null)
->orderBy('wfc.calculated_at', 'DESC')
->get()
->getResultArray();
} catch (\Throwable $e) {
log_message('error', 'Pending withdrawal review lookup failed: ' . $e->getMessage());
return [];
}
}
private function repairPendingWithdrawalRefunds(): void
{
try {
@@ -1021,24 +1098,48 @@ class RefundController extends BaseController
continue;
}
$students = $this->enrollmentModel
->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->findAll();
$refundAmount = $this->feeCalculationService->calculateRefund($students, $parentId);
$refundCents = max(0, (int)round($refundAmount * 100));
$this->refundModel->update((int)$refund['id'], [
'invoice_id' => (int)$invoice['id'],
'refund_amount' => $refundAmount,
'requested_amount_cents' => $refundCents,
'currency' => 'USD',
'request' => 'tuition',
'source_type' => 'tuition_withdrawal',
'source_id' => (int)$invoice['id'],
'reconciliation_status' => 'requires_review',
'reconciliation_reason' => 'Legacy pending withdrawal refund requires recalculation through the withdrawal financial review workflow.',
'reconciliation_required_at' => utc_now(),
'updated_at' => utc_now(),
]);
}
if ($this->db->tableExists('withdrawal_financial_calculations')) {
$postedRows = $this->db->table('refunds r')
->select('r.id, r.refund_amount, r.requested_amount_cents, wfc.posted_at, wfc.posted_by')
->join('withdrawal_financial_calculations wfc', 'wfc.id = r.withdrawal_calculation_id', 'inner')
->where('r.source_type', 'tuition_withdrawal')
->whereIn('r.status', ['Pending', 'pending', 'requested'])
->where('wfc.status', 'posted')
->where('wfc.posted_at IS NOT NULL', null, false)
->get()
->getResultArray();
foreach ($postedRows as $refund) {
$amountCents = (int)($refund['requested_amount_cents'] ?? 0);
if ($amountCents <= 0) {
$amountCents = (int)round(((float)($refund['refund_amount'] ?? 0)) * 100);
}
if ($amountCents <= 0) {
continue;
}
$this->refundModel->update((int)$refund['id'], [
'status' => $this->refundStatusForStorage(FinancialStatus::REFUND_APPROVED),
'approved_amount_cents' => $amountCents,
'approved_at' => $refund['posted_at'] ?? utc_now(),
'approved_by' => !empty($refund['posted_by']) ? (int)$refund['posted_by'] : null,
'updated_at' => utc_now(),
]);
}
}
} catch (\Throwable $e) {
log_message('error', 'Pending withdrawal refund repair failed: ' . $e->getMessage());
}