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
+130 -45
View File
@@ -3,6 +3,41 @@
<div class="container-fluid mt-4">
<h2 class="text-center mt-4 mb-3">Refunds</h2>
<?php
$withdrawalReviews = $withdrawalReviews ?? [];
$refunds = $refunds ?? [];
$moneyFromCents = static fn($cents): string => '$' . number_format(((int) $cents) / 100, 2);
$dateOnly = static function($dt) {
if (empty($dt) || $dt === '0000-00-00 00:00:00') return '-';
return local_date($dt, 'm-d-Y');
};
$normalizeRefundStatus = static function($status): string {
$key = strtolower(str_replace(' ', '_', trim((string) $status)));
if ($key === 'requested') return 'pending';
if ($key === 'partially_paid') return 'partial';
return $key !== '' ? $key : 'pending';
};
$summary = [
'reviews' => count($withdrawalReviews),
'approval' => 0,
'payment' => 0,
'complete' => 0,
];
foreach ($refunds as $refund) {
$key = $normalizeRefundStatus($refund['status'] ?? '');
$isWithdrawalRefund = (string)($refund['source_type'] ?? '') === 'tuition_withdrawal';
$amount = (float) ($refund['refund_amount'] ?? 0);
$paid = (float) ($refund['refund_paid_amount'] ?? 0);
$remaining = max(0, $amount - $paid);
if ($key === 'pending' && !$isWithdrawalRefund) {
$summary['approval']++;
} elseif (in_array($key, ['approved', 'partial'], true) && $remaining > 0) {
$summary['payment']++;
} elseif (in_array($key, ['paid', 'rejected'], true)) {
$summary['complete']++;
}
}
?>
<!-- Flash messages -->
<?php if (session()->getFlashdata('success')): ?>
@@ -15,34 +50,78 @@
<div class="alert alert-info mb-3 d-inline-block"><?= session()->getFlashdata('info') ?></div>
<?php endif; ?>
<!-- (Optional) Bulk calculation form kept for future use -->
<form method="post" action="/refunds/processRefunds">
<?= csrf_field() ?>
<?php
$submitted = [];
foreach ($refunds as $refund):
if (!empty($refund['request']) && !in_array($refund['parent_id'], $submitted, true)):
$submitted[] = $refund['parent_id'];
?>
<input type="hidden" name="parent_ids[]" value="<?= esc($refund['parent_id']) ?>">
<?php
endif;
endforeach;
?>
<?php if (empty($submitted)): ?>
<div class="alert alert-info mb-3 d-inline-block">No eligible parents for refund calculation.</div>
<?php endif; ?>
</form>
<div class="row g-3 mb-4">
<div class="col-md-3"><div class="card h-100"><div class="card-body"><div class="text-muted small">Withdrawal reviews</div><div class="fs-4 fw-semibold"><?= (int) $summary['reviews'] ?></div></div></div></div>
<div class="col-md-3"><div class="card h-100"><div class="card-body"><div class="text-muted small">Need approval</div><div class="fs-4 fw-semibold"><?= (int) $summary['approval'] ?></div></div></div></div>
<div class="col-md-3"><div class="card h-100"><div class="card-body"><div class="text-muted small">Ready to pay</div><div class="fs-4 fw-semibold"><?= (int) $summary['payment'] ?></div></div></div></div>
<div class="col-md-3"><div class="card h-100"><div class="card-body"><div class="text-muted small">Closed</div><div class="fs-4 fw-semibold"><?= (int) $summary['complete'] ?></div></div></div></div>
</div>
<?php if (!empty($withdrawalReviews)): ?>
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Withdrawal Review Queue</strong>
<span class="badge bg-warning text-dark"><?= count($withdrawalReviews) ?> pending</span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-striped align-middle w-100 mb-0 no-mgmt-sticky no-dt-fixedheader" data-no-mgmt-sticky data-no-dt-fixedheader>
<thead>
<tr>
<th>Parent</th>
<th>Student</th>
<th>Invoice&nbsp;#</th>
<th>Requested</th>
<th>Expected Refund</th>
<th>Balance Due</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($withdrawalReviews as $review): ?>
<?php
$reviewStatus = (string) ($review['status'] ?? '');
$reviewStatusClass = $reviewStatus === 'requires_review' ? 'danger' : 'warning text-dark';
?>
<tr>
<td><?= esc(trim(($review['parent_firstname'] ?? '') . ' ' . ($review['parent_lastname'] ?? '')) ?: '-') ?></td>
<td><?= esc(trim(($review['student_firstname'] ?? '') . ' ' . ($review['student_lastname'] ?? '')) ?: '-') ?></td>
<td><?= esc($review['invoice_number'] ?? '-') ?></td>
<td>
<div><?= esc($dateOnly($review['withdrawal_request_date'] ?? null)) ?></div>
<div class="text-muted small">Calculated <?= esc($dateOnly($review['calculated_at'] ?? null)) ?></div>
</td>
<td class="fw-semibold"><?= esc($moneyFromCents($review['new_refund_request_cents'] ?? 0)) ?></td>
<td><?= esc($moneyFromCents($review['balance_due_cents'] ?? 0)) ?></td>
<td><span class="badge bg-<?= esc($reviewStatusClass) ?>"><?= esc($reviewStatus ?: 'preview') ?></span></td>
<td>
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-primary btn-sm" href="<?= site_url('administrator/withdrawals/' . (int) $review['enrollment_id'] . '/review') ?>">Open Review</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-2">
<h4 class="mb-0">Refund Queue</h4>
</div>
<div class="table-responsive">
<table id="refundsTable" class="table table-bordered table-striped align-middle w-100">
<table id="refundsTable" class="table table-bordered table-striped align-middle w-100 no-mgmt-sticky no-dt-fixedheader" data-no-mgmt-sticky data-no-dt-fixedheader>
<thead>
<tr>
<th>Parent</th>
<th>Invoice&nbsp;#</th>
<th>Requested</th>
<th>Amount</th>
<th>Status</th>
<th>Refund Details</th>
<th>Payment</th>
<th>Actions</th>
</tr>
</thead>
@@ -56,12 +135,7 @@
return local_date($dt, 'm-d-Y');
};
$statusRaw = (string)($r['status'] ?? '');
$statusKey = strtolower(str_replace(' ', '_', trim($statusRaw)));
if ($statusKey === 'requested') {
$statusKey = 'pending';
} elseif ($statusKey === 'partially_paid') {
$statusKey = 'partial';
}
$statusKey = $normalizeRefundStatus($statusRaw);
$statusLabel = [
'pending' => 'Pending',
'approved' => 'Approved',
@@ -80,9 +154,20 @@
$paidAmount = (float)($r['refund_paid_amount'] ?? 0);
$remainingAmount = max(0, $refundAmount - $paidAmount);
$hasSource = !empty($r['source_type']) && !empty($r['source_id']);
$canApprove = $statusKey === 'pending' && $refundAmount > 0 && $hasSource;
$canReject = $statusKey === 'pending';
$isWithdrawalRefund = (string)($r['source_type'] ?? '') === 'tuition_withdrawal';
$canApprove = !$isWithdrawalRefund && $statusKey === 'pending' && $refundAmount > 0 && $hasSource;
$canReject = !$isWithdrawalRefund && $statusKey === 'pending';
$canRecord = in_array($statusKey, ['approved', 'partial'], true) && $remainingAmount > 0;
$nextStep = 'Closed';
if ($statusKey === 'pending') {
$nextStep = $isWithdrawalRefund ? 'Approved by withdrawal review' : ($canApprove ? 'Needs approval' : 'Needs review');
} elseif ($canRecord) {
$nextStep = 'Ready to pay';
} elseif ($statusKey === 'paid') {
$nextStep = 'Paid';
} elseif ($statusKey === 'rejected') {
$nextStep = 'Rejected';
}
?>
<tr>
<td><?= esc(($r['firstname'] ?? '').' '.($r['lastname'] ?? '')) ?></td>
@@ -93,6 +178,7 @@
</td>
<td>
<div><span class="badge bg-<?= esc($statusClass) ?>"><?= esc($statusLabel) ?></span></div>
<div class="text-muted small mt-1"><?= esc($nextStep) ?></div>
<?php if (!empty($r['approved_at']) && $r['approved_at'] !== '0000-00-00 00:00:00'): ?>
<div class="text-muted small mt-1">
<?= esc($fmt($r['approved_at'])) ?>
@@ -103,6 +189,8 @@
<?php endif; ?>
</td>
<td>
<div><span class="text-muted small">Paid:</span> $<?= esc(number_format($paidAmount, 2)) ?></div>
<div><span class="text-muted small">Remaining:</span> $<?= esc(number_format($remainingAmount, 2)) ?></div>
<div><span class="text-muted small">Check #:</span> <?= esc($r['check_nbr'] ?? '-') ?></div>
<div>
<span class="text-muted small">Check File:</span>
@@ -112,25 +200,21 @@
-
<?php endif; ?>
</div>
<div><span class="text-muted small">Paid:</span> $<?= esc(number_format($paidAmount, 2)) ?></div>
</td>
<td>
<div class="d-flex gap-2 flex-wrap">
<button class="btn btn-success btn-sm"
<?= $canRecord ? '' : 'disabled' ?>
onclick="handleRecordRefundClick(<?= (int)$r['id'] ?>, '<?= esc($statusLabel) ?>', <?= $remainingAmount ?>)">
Record
</button>
<button class="btn btn-primary btn-sm"
<?= $canApprove ? '' : 'disabled' ?>
onclick="handleStatusClick(<?= (int)$r['id'] ?>, 'Approved', <?= $refundAmount ?>)">
Approve
</button>
<button class="btn btn-danger btn-sm"
<?= $canReject ? '' : 'disabled' ?>
onclick="handleStatusClick(<?= (int)$r['id'] ?>, 'Rejected', <?= max($refundAmount, 0.01) ?>)">
Reject
</button>
<?php if ($canRecord): ?>
<button class="btn btn-success btn-sm" onclick="handleRecordRefundClick(<?= (int)$r['id'] ?>, '<?= esc($statusLabel) ?>', <?= $remainingAmount ?>)">Pay</button>
<?php endif; ?>
<?php if ($canApprove): ?>
<button class="btn btn-primary btn-sm" onclick="handleStatusClick(<?= (int)$r['id'] ?>, 'Approved', <?= $refundAmount ?>)">Approve</button>
<?php endif; ?>
<?php if ($canReject): ?>
<button class="btn btn-outline-danger btn-sm" onclick="handleStatusClick(<?= (int)$r['id'] ?>, 'Rejected', <?= max($refundAmount, 0.01) ?>)">Reject</button>
<?php endif; ?>
<?php if (!$canRecord && !$canApprove && !$canReject): ?>
<span class="text-muted small">No action</span>
<?php endif; ?>
</div>
</td>
</tr>
@@ -226,6 +310,7 @@ $(function () {
order: [[2, 'desc']], // order by Requested desc
scrollX: true,
autoWidth: false,
fixedHeader: false,
});
// Status form (approve/reject)