fix deployment db issue and widthrawal administration page
This commit is contained in:
@@ -8,6 +8,7 @@ use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
use App\Support\Enrollment\DeliberationDecision;
|
||||
|
||||
class EnrollmentAdminController extends BaseController
|
||||
{
|
||||
@@ -503,12 +504,67 @@ class EnrollmentAdminController extends BaseController
|
||||
$row['assignee_name'] = trim((string) ($row['assignee_firstname'] ?? '') . ' ' . (string) ($row['assignee_lastname'] ?? ''));
|
||||
$row['parent_name'] = trim((string) ($row['parent_firstname'] ?? '') . ' ' . (string) ($row['parent_lastname'] ?? '')) ?: ((int) ($row['parent_id'] ?? 0) > 0 ? 'Parent #' . (int) $row['parent_id'] : '');
|
||||
$row['details'] = json_decode((string) ($row['details_json'] ?? ''), true) ?: [];
|
||||
if (! isset($row['details']['rule_code'])) {
|
||||
$ruleCode = $this->ruleCodeForExistingFlag($row);
|
||||
if ($ruleCode !== null) {
|
||||
$row['details']['rule_code'] = $ruleCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
private function ruleCodeForExistingFlag(array $flag): ?string
|
||||
{
|
||||
$type = strtoupper(trim((string) ($flag['flag_type'] ?? '')));
|
||||
if (! in_array($type, ['DEFERRED_DELIBERATION', 'RESTRICTED_ADMINISTRATIVE_REVIEW', 'WITHDRAWAL_REVIEW_REQUIRED', 'PENDING_MAKE_UP_EXAM_PROMOTION'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$studentId = (int) ($flag['student_id'] ?? 0);
|
||||
$sourceSchoolYear = trim((string) ($flag['source_school_year'] ?? ''));
|
||||
if ($studentId <= 0 || $sourceSchoolYear === '' || ! $this->db->tableExists('student_decisions')) {
|
||||
return $type === 'WITHDRAWAL_REVIEW_REQUIRED' ? 'WITHDRAWN' : null;
|
||||
}
|
||||
|
||||
$select = ['decision'];
|
||||
if ($this->db->fieldExists('deliberation_decision_standard', 'student_decisions')) {
|
||||
$select[] = 'deliberation_decision_standard';
|
||||
}
|
||||
|
||||
$row = $this->db->table('student_decisions')
|
||||
->select($select)
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $sourceSchoolYear)
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(1)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($row === null) {
|
||||
return match ($type) {
|
||||
'DEFERRED_DELIBERATION' => 'NO_FINAL_DECISION',
|
||||
'WITHDRAWAL_REVIEW_REQUIRED' => 'WITHDRAWN',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
$decision = DeliberationDecision::normalize($row['deliberation_decision_standard'] ?? null)
|
||||
?? DeliberationDecision::normalize($row['decision'] ?? null);
|
||||
|
||||
return match ($decision) {
|
||||
DeliberationDecision::DEFERRED_DECISION => 'DEFERRED_DECISION',
|
||||
DeliberationDecision::EXPELLED => 'EXPELLED',
|
||||
DeliberationDecision::WITHDRAWN => 'WITHDRAWN',
|
||||
DeliberationDecision::MAKE_UP_EXAM => 'MAKE_UP_EXAM',
|
||||
null => $type === 'DEFERRED_DELIBERATION' ? 'UNRECOGNIZED_DECISION' : null,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function enrollmentFollowups(string $schoolYear, array $flags = []): array
|
||||
{
|
||||
if (! $this->db->tableExists('enrollments')) {
|
||||
|
||||
@@ -514,9 +514,16 @@ class InvoiceController extends ResourceController
|
||||
bool $recalculateDiscounts = true
|
||||
)
|
||||
{
|
||||
$isAjax = $this->request->isAJAX() || str_contains(strtolower($this->request->getHeaderLine('Accept')), 'application/json');
|
||||
$request = $this->request ?? service('request');
|
||||
$isAjax = $request !== null && (
|
||||
$request->isAJAX()
|
||||
|| str_contains(strtolower($request->getHeaderLine('Accept')), 'application/json')
|
||||
);
|
||||
// Programmatic callers (new InvoiceController() without initController) have no response object.
|
||||
$hasHttpResponse = $this->response !== null;
|
||||
|
||||
if ($parentId == null) {
|
||||
$parentId = (int)$this->request->getPost('parent_id');
|
||||
$parentId = (int) ($request?->getPost('parent_id') ?? 0);
|
||||
}
|
||||
$schoolYear = (string) ($schoolYearOverride ?: $this->schoolYear);
|
||||
$semester = (string) ($semesterOverride ?: $this->semester);
|
||||
@@ -528,10 +535,13 @@ class InvoiceController extends ResourceController
|
||||
->findAll();
|
||||
|
||||
if (empty($enrollments)) {
|
||||
if ($isAjax) {
|
||||
return $this->response->setJSON(['ok' => false, 'message' => 'No enrollment records found.']);
|
||||
}
|
||||
return redirect()->back()->with('error', 'No enrollment records found.');
|
||||
return $this->invoiceGenerationResult(
|
||||
$hasHttpResponse,
|
||||
$isAjax,
|
||||
['ok' => false, 'message' => 'No enrollment records found.'],
|
||||
422,
|
||||
'No enrollment records found.'
|
||||
);
|
||||
}
|
||||
|
||||
$registeredKids = [];
|
||||
@@ -605,6 +615,24 @@ class InvoiceController extends ResourceController
|
||||
log_message('info', "Updated invoice ID {$invoice['id']} for parent ID {$parentId}.");
|
||||
$updated = true;
|
||||
} else {
|
||||
$hasNonZeroTuitionOrEvents = abs((float) $tuitionFee) > 0.00001
|
||||
|| abs((float) $eventchargeTotal) > 0.00001;
|
||||
$hasApprovedAdjustments = $this->parentHasApprovedInvoiceAdjustments(
|
||||
(int) $parentId,
|
||||
$schoolYear,
|
||||
$semester
|
||||
);
|
||||
|
||||
if (! $hasNonZeroTuitionOrEvents && ! $hasApprovedAdjustments) {
|
||||
return $this->invoiceGenerationResult(
|
||||
$hasHttpResponse,
|
||||
$isAjax,
|
||||
['ok' => false, 'message' => 'Invoice requires at least one non-zero line.'],
|
||||
422,
|
||||
'Invoice requires at least one non-zero line.'
|
||||
);
|
||||
}
|
||||
|
||||
$issueUtc = (new DateTime('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
|
||||
|
||||
// Due date: interpret the date in configured/user local TZ,
|
||||
@@ -642,29 +670,95 @@ class InvoiceController extends ResourceController
|
||||
log_message('info', "Invoice created successfully. Insert ID: {$insertId}");
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Invoice issuance failed: ' . $e->getMessage() . ' errors=' . json_encode($this->invoiceModel->errors()));
|
||||
if ($isAjax) {
|
||||
return $this->response->setJSON(['ok' => false, 'message' => 'Failed to create invoice.']);
|
||||
}
|
||||
return redirect()->back()->with('error', 'Failed to create invoice. Please check input values.');
|
||||
$message = str_contains($e->getMessage(), 'non-zero invoice line')
|
||||
? 'Invoice requires at least one non-zero line.'
|
||||
: 'Failed to create invoice.';
|
||||
|
||||
return $this->invoiceGenerationResult(
|
||||
$hasHttpResponse,
|
||||
$isAjax,
|
||||
['ok' => false, 'message' => $message],
|
||||
422,
|
||||
$message === 'Invoice requires at least one non-zero line.'
|
||||
? $message
|
||||
: 'Failed to create invoice. Please check input values.'
|
||||
);
|
||||
}
|
||||
$updated = false;
|
||||
}
|
||||
|
||||
// Success response
|
||||
$successPayload = [
|
||||
'ok' => true,
|
||||
'updated' => $updated,
|
||||
'updated_ids' => $updatedIds,
|
||||
'insert_id' => isset($insertId) ? (int)$insertId : null,
|
||||
csrf_token() => csrf_hash(),
|
||||
'csrfTokenName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
];
|
||||
|
||||
return $this->invoiceGenerationResult(
|
||||
$hasHttpResponse,
|
||||
$isAjax,
|
||||
$successPayload,
|
||||
200,
|
||||
null,
|
||||
$updated ? 'Invoice updated.' : 'Invoice created.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe invoice response helper for both HTTP and programmatic callers.
|
||||
*
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function invoiceGenerationResult(
|
||||
bool $hasHttpResponse,
|
||||
bool $isAjax,
|
||||
array $payload,
|
||||
int $statusCode = 200,
|
||||
?string $errorFlash = null,
|
||||
?string $successFlash = null
|
||||
) {
|
||||
if (! $hasHttpResponse || $this->response === null) {
|
||||
return $payload;
|
||||
}
|
||||
|
||||
if ($isAjax) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => true,
|
||||
'updated' => $updated,
|
||||
'updated_ids' => $updatedIds,
|
||||
'insert_id' => isset($insertId) ? (int)$insertId : null,
|
||||
csrf_token() => csrf_hash(),
|
||||
'csrfTokenName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
return $this->response
|
||||
->setStatusCode($statusCode)
|
||||
->setJSON($payload);
|
||||
}
|
||||
|
||||
if ($errorFlash !== null) {
|
||||
return redirect()->back()->with('error', $errorFlash);
|
||||
}
|
||||
|
||||
return redirect()->to(route_to('InvoiceController::index'))
|
||||
->with('success', $updated ? 'Invoice updated.' : 'Invoice created.');
|
||||
->with('success', $successFlash ?? 'Invoice saved.');
|
||||
}
|
||||
|
||||
private function parentHasApprovedInvoiceAdjustments(int $parentId, string $schoolYear, string $semester): bool
|
||||
{
|
||||
if ($parentId <= 0 || $schoolYear === '' || $semester === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->additionalChargeModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->where('status', FinancialStatus::ADDITIONAL_CHARGE_APPROVED)
|
||||
->where('amount !=', 0)
|
||||
->countAllResults() > 0;
|
||||
} catch (\Throwable $e) {
|
||||
log_message('warning', 'Unable to check approved invoice adjustments: {message}', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function selectActiveInvoiceForParentYear(int $parentId, string $schoolYear): ?array
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
use App\Controllers\BaseController;
|
||||
use Config\SessionTimeout;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
|
||||
class SessionTimeoutController extends BaseController
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\SessionTimeout;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Lightweight session heartbeat endpoints.
|
||||
* Intentionally does not extend BaseController to avoid school-year / API priming.
|
||||
*/
|
||||
class SessionTimeoutController extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
|
||||
{
|
||||
parent::initController($request, $response, $logger);
|
||||
session();
|
||||
}
|
||||
|
||||
public function getTimeoutConfig()
|
||||
{
|
||||
return $this->response->setJSON([
|
||||
@@ -17,77 +31,121 @@ class SessionTimeoutController extends BaseController
|
||||
'timeout' => SessionTimeout::TIMEOUT_DURATION,
|
||||
'warning_time' => SessionTimeout::TIMEOUT_DURATION - SessionTimeout::WARNING_THRESHOLD,
|
||||
'check_interval' => SessionTimeout::CLIENT_CHECK_INTERVAL,
|
||||
'ping_interval' => SessionTimeout::CLIENT_PING_INTERVAL,
|
||||
'logout_url' => site_url('logout'),
|
||||
'keep_alive_url' => site_url('session/ping-activity'),
|
||||
'check_url' => site_url('session/check-timeout')
|
||||
'check_url' => site_url('session/check-timeout'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function checkTimeout()
|
||||
{
|
||||
$session = session();
|
||||
try {
|
||||
$session = session();
|
||||
|
||||
// Verify session exists and has last_activity
|
||||
if (!$session->has('last_activity')) {
|
||||
return $this->expireSession();
|
||||
}
|
||||
if (! $session->has('last_activity')) {
|
||||
return $this->expireSession();
|
||||
}
|
||||
|
||||
$lastActivity = $session->get('last_activity');
|
||||
$elapsed = time() - $lastActivity;
|
||||
$lastActivity = (int) $session->get('last_activity');
|
||||
$elapsed = time() - $lastActivity;
|
||||
|
||||
if ($elapsed >= SessionTimeout::TIMEOUT_DURATION) {
|
||||
return $this->expireSession();
|
||||
}
|
||||
|
||||
if ($elapsed >= SessionTimeout::WARNING_THRESHOLD) {
|
||||
return $this->response->setJSON([
|
||||
'status' => 'warning',
|
||||
'time_remaining' => SessionTimeout::TIMEOUT_DURATION - $elapsed,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($elapsed >= SessionTimeout::TIMEOUT_DURATION) {
|
||||
return $this->expireSession();
|
||||
} elseif ($elapsed >= SessionTimeout::WARNING_THRESHOLD) {
|
||||
return $this->response->setJSON([
|
||||
'status' => 'warning',
|
||||
'time_remaining' => SessionTimeout::TIMEOUT_DURATION - $elapsed
|
||||
'status' => 'active',
|
||||
'time_remaining' => SessionTimeout::TIMEOUT_DURATION - $elapsed,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
$this->logDbFailureOnce('session/check-timeout', $e);
|
||||
|
||||
return $this->response
|
||||
->setStatusCode(503)
|
||||
->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => 'Session check temporarily unavailable.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 'active',
|
||||
'time_remaining' => SessionTimeout::TIMEOUT_DURATION - $elapsed
|
||||
]);
|
||||
}
|
||||
|
||||
public function pingActivity()
|
||||
{
|
||||
$session = session();
|
||||
|
||||
// Only update if session is still valid
|
||||
if (!$session->has('last_activity') ||
|
||||
(time() - $session->get('last_activity') >= SessionTimeout::TIMEOUT_DURATION)) {
|
||||
return $this->expireSession();
|
||||
try {
|
||||
$session = session();
|
||||
|
||||
if (! $session->has('last_activity')
|
||||
|| (time() - (int) $session->get('last_activity') >= SessionTimeout::TIMEOUT_DURATION)
|
||||
) {
|
||||
return $this->expireSession();
|
||||
}
|
||||
|
||||
$session->set('last_activity', time());
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 'active',
|
||||
'time_remaining' => SessionTimeout::TIMEOUT_DURATION,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
$this->logDbFailureOnce('session/ping-activity', $e);
|
||||
|
||||
return $this->response
|
||||
->setStatusCode(503)
|
||||
->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => 'Activity ping temporarily unavailable.',
|
||||
]);
|
||||
}
|
||||
|
||||
$session->set('last_activity', time());
|
||||
return $this->response->setJSON([
|
||||
'status' => 'active',
|
||||
'time_remaining' => SessionTimeout::TIMEOUT_DURATION
|
||||
]);
|
||||
}
|
||||
|
||||
private function expireSession()
|
||||
{
|
||||
$this->destroySession();
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 'expired',
|
||||
'redirect' => site_url('login'),
|
||||
'message' => 'Your session has expired due to inactivity.'
|
||||
'message' => 'Your session has expired due to inactivity.',
|
||||
]);
|
||||
}
|
||||
|
||||
private function destroySession()
|
||||
private function destroySession(): void
|
||||
{
|
||||
$session = session();
|
||||
$session->setFlashdata('error', 'Your session has expired due to inactivity.');
|
||||
|
||||
// Clear session data
|
||||
$session->remove('last_activity');
|
||||
$session->destroy();
|
||||
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_regenerate_id(true);
|
||||
try {
|
||||
$session = session();
|
||||
$session->setFlashdata('error', 'Your session has expired due to inactivity.');
|
||||
$session->remove('last_activity');
|
||||
$session->destroy();
|
||||
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->logDbFailureOnce('session/destroy', $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function logDbFailureOnce(string $endpoint, Throwable $e): void
|
||||
{
|
||||
static $logged = [];
|
||||
|
||||
$key = $endpoint . '|' . get_class($e);
|
||||
if (isset($logged[$key])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$logged[$key] = true;
|
||||
log_message('error', 'Session endpoint failure on {endpoint}: {message}', [
|
||||
'endpoint' => $endpoint,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user