Merge branch 'dev' of bitbucket.org:jubilian/nhance-enrollment into dev
This commit is contained in:
commit
40a58e148f
117
app/Commands/TestEmployeeUploadDates.php
Normal file
117
app/Commands/TestEmployeeUploadDates.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Manual integration script for employeesUplodWithEvents.
|
||||
*
|
||||
* Usage:
|
||||
* php spark test:employee-upload-dates
|
||||
*
|
||||
* Inserts a real files row using sample enrollment excel and prints stored enrollment dates.
|
||||
*/
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Controllers\EmployeeController;
|
||||
use CodeIgniter\CLI\BaseCommand;
|
||||
use CodeIgniter\CLI\CLI;
|
||||
use Config\Database;
|
||||
use Tests\Support\TestUploadedFile;
|
||||
|
||||
class TestEmployeeUploadDates extends BaseCommand
|
||||
{
|
||||
protected $group = 'Testing';
|
||||
protected $name = 'test:employee-upload-dates';
|
||||
protected $description = 'Test employeesUplodWithEvents with d-m-Y and d/m/Y enrollment dates and DB insert';
|
||||
protected $usage = 'test:employee-upload-dates [--open=01-07-2026] [--close=31/07/2026]';
|
||||
|
||||
protected $options = [
|
||||
'--open' => 'Enrollment open date (d-m-Y or d/m/Y)',
|
||||
'--close' => 'Enrollment close date (d-m-Y or d/m/Y)',
|
||||
];
|
||||
|
||||
public function run(array $params)
|
||||
{
|
||||
helper('utility');
|
||||
|
||||
$openDate = CLI::getOption('open') ?? '01-07-2026';
|
||||
$closeDate = CLI::getOption('close') ?? '31/07/2026';
|
||||
$sample = ROOTPATH . 'public/sample_excel/enrollment.xlsx';
|
||||
|
||||
if (!is_file($sample)) {
|
||||
CLI::error('Sample file not found: ' . $sample);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
require_once ROOTPATH . 'tests/_support/TestUploadedFile.php';
|
||||
|
||||
$db = Database::connect('default');
|
||||
$row = $db->query(
|
||||
'SELECT client_id, policy_id, client_branch_id, created_by
|
||||
FROM files
|
||||
WHERE client_id IS NOT NULL
|
||||
AND policy_id IS NOT NULL
|
||||
AND client_branch_id IS NOT NULL
|
||||
AND created_by IS NOT NULL
|
||||
ORDER BY id DESC
|
||||
LIMIT 1'
|
||||
)->getRowArray();
|
||||
|
||||
if (empty($row)) {
|
||||
CLI::error('No fixture row found in files table.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
CLI::write('Input dates:', 'yellow');
|
||||
CLI::write(" open : {$openDate}");
|
||||
CLI::write(" close : {$closeDate}");
|
||||
CLI::write('Converted:', 'yellow');
|
||||
CLI::write(' open : ' . change_date_format($openDate, null, 'Y-m-d'));
|
||||
CLI::write(' close : ' . change_date_format($closeDate, null, 'Y-m-d'));
|
||||
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'enroll_manual_');
|
||||
copy($sample, $tmpPath);
|
||||
|
||||
$uploadedFile = new TestUploadedFile(
|
||||
$tmpPath,
|
||||
basename($sample),
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
filesize($tmpPath),
|
||||
UPLOAD_ERR_OK
|
||||
);
|
||||
|
||||
$postData = [
|
||||
'client_id' => (int) $row['client_id'],
|
||||
'policy_id' => (int) $row['policy_id'],
|
||||
'client_branch_id' => (int) $row['client_branch_id'],
|
||||
'created_by' => (int) $row['created_by'],
|
||||
'enrollment_open_date' => $openDate,
|
||||
'enrollment_close_date' => $closeDate,
|
||||
'emplist' => $uploadedFile,
|
||||
];
|
||||
|
||||
$controller = new EmployeeController();
|
||||
$result = $controller->employeesUplodWithEvents($postData);
|
||||
|
||||
CLI::newLine();
|
||||
CLI::write('Upload result:', 'green');
|
||||
CLI::write(json_encode($result, JSON_PRETTY_PRINT));
|
||||
|
||||
if (empty($result['file_id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$file = $db->table('files')->where('id', $result['file_id'])->get()->getRowArray();
|
||||
CLI::newLine();
|
||||
CLI::write('DB record:', 'green');
|
||||
CLI::write(json_encode([
|
||||
'id' => $file['id'] ?? null,
|
||||
'file_name' => $file['file_name'] ?? null,
|
||||
'action' => $file['action'] ?? null,
|
||||
'status' => $file['status'] ?? null,
|
||||
'enrollment_open_date' => $file['enrollment_open_date'] ?? null,
|
||||
'enrollment_close_date' => $file['enrollment_close_date'] ?? null,
|
||||
], JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
@ -73,6 +73,12 @@ class Acl
|
||||
'teams' => []
|
||||
],
|
||||
|
||||
// ===================== LOGIN DIAGNOSTIC =====================
|
||||
'#^/security/login-diagnostic#' => [
|
||||
'roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID],
|
||||
'teams' => []
|
||||
],
|
||||
|
||||
// ===================== INTERNAL TEST =====================
|
||||
'#^/test#' => [
|
||||
'roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID],
|
||||
|
||||
@ -594,6 +594,11 @@ $routes->group('security/rate-limits', ['filter' => 'authMVC'], function ($route
|
||||
$routes->post('unblock-user', 'RateLimitAdminController::unblockUser');
|
||||
});
|
||||
|
||||
$routes->group('security/login-diagnostic', ['filter' => 'authMVC'], function ($routes) {
|
||||
$routes->get('/', 'LoginDiagnosticController::index');
|
||||
$routes->post('diagnose', 'LoginDiagnosticController::diagnose');
|
||||
});
|
||||
|
||||
//saml - routes
|
||||
// $routes->group("employeeRest", ['filter' => ['appSignature','AuthApiRateLimitFilter'] ], function ($routes) {
|
||||
|
||||
|
||||
@ -397,8 +397,8 @@ class EmployeeController extends AdminController
|
||||
$status = 'inprogress';
|
||||
$hr_id = $post_data['created_by'] ?? null;
|
||||
|
||||
$enrollment_open_date = change_date_format($enrollment_open_date, 'd/m/Y', 'Y-m-d');
|
||||
$enrollment_close_date = change_date_format($enrollment_close_date, 'd/m/Y', 'Y-m-d');
|
||||
$enrollment_open_date = change_date_format($enrollment_open_date, null, 'Y-m-d');
|
||||
$enrollment_close_date = change_date_format($enrollment_close_date, null, 'Y-m-d');
|
||||
|
||||
$file_id = $this->fileModel->insert(['file_name' => $filename, 'client_id' => $client_id, 'policy_id' => $policy_id, 'created_by' => $loggedInUserID, 'status' => $status, 'action' => $action, 'client_branch_id' => $branch_id, 'uploaded_by' => 1, 'enrollment_open_date' => $enrollment_open_date, 'enrollment_close_date' => $enrollment_close_date, 'hr_id' => $hr_id]); //here field policy_id have client_policy_id and not policy id from policy master
|
||||
$this->myLogger->logme("error", '{file_id} uploaded success', ['file_id' => $file_id]);
|
||||
|
||||
@ -3251,11 +3251,16 @@ class EmployeeRestController extends AdminController
|
||||
$value['insurer_name'] = $insurerData->name ?? null;
|
||||
$value['insurer_short_name'] = $insurerData->short_name ?? null;
|
||||
$employeeDetails = $this->employeePolicyModel->getEmployeePolicy( client_id:$value['client_id'],policy_id: $value['client_policy_id'],status:0,branch_id:$client_branch_id);
|
||||
$totalCount = 0;
|
||||
$enrolledCount = 0;
|
||||
$draftCount = 0;
|
||||
if(count($employeeDetails))
|
||||
{
|
||||
foreach ($employeeDetails as $item) {
|
||||
if ($item['relationship'] !== 'Self') {
|
||||
continue;
|
||||
}
|
||||
$totalCount++;
|
||||
if ($item['emp_status'] === 'enrolled') {
|
||||
$enrolledCount++;
|
||||
} elseif ($item['emp_status'] === 'draft') {
|
||||
@ -3264,7 +3269,7 @@ class EmployeeRestController extends AdminController
|
||||
}
|
||||
}
|
||||
|
||||
$value['totalMembersCount'] = count($employeeDetails);
|
||||
$value['totalMembersCount'] = $totalCount;
|
||||
$value['membersCountOfEnrolled'] = $enrolledCount;
|
||||
$value['membersCountOfDraft'] = $draftCount;
|
||||
$value['membersCountOfLoggedIn'] = $loggedInCounts[$value['client_policy_id']] ?? 0;
|
||||
|
||||
68
app/Controllers/LoginDiagnosticController.php
Normal file
68
app/Controllers/LoginDiagnosticController.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Helpers\LoginDiagnosticHelper;
|
||||
|
||||
/**
|
||||
* Admin-only employee login diagnostic (PRE + POST).
|
||||
* Does not modify existing auth controllers/models/views.
|
||||
*/
|
||||
class LoginDiagnosticController extends BaseController
|
||||
{
|
||||
public function index(): void
|
||||
{
|
||||
$email = trim((string) $this->request->getGet('email'));
|
||||
$mobile = trim((string) $this->request->getGet('mobile'));
|
||||
|
||||
$data = [
|
||||
'title' => 'Employee Login Diagnostic',
|
||||
'email' => $email,
|
||||
'mobile' => $mobile,
|
||||
'result' => null,
|
||||
'has_search' => false,
|
||||
];
|
||||
|
||||
if ($email !== '' || $mobile !== '') {
|
||||
$data['has_search'] = true;
|
||||
$data['result'] = LoginDiagnosticHelper::diagnose(
|
||||
$email !== '' ? $email : null,
|
||||
$mobile !== '' ? $mobile : null
|
||||
);
|
||||
}
|
||||
|
||||
echo view('admin/login_diagnostic', $data);
|
||||
}
|
||||
|
||||
public function diagnose()
|
||||
{
|
||||
$email = trim((string) $this->request->getPost('email'));
|
||||
$mobile = trim((string) $this->request->getPost('mobile'));
|
||||
|
||||
if ($email === '' && $mobile === '') {
|
||||
$json = $this->request->getJSON(true);
|
||||
if (is_array($json)) {
|
||||
$email = trim((string) ($json['email'] ?? ''));
|
||||
$mobile = trim((string) ($json['mobile'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->request->isAJAX() || str_contains((string) $this->request->getHeaderLine('Accept'), 'application/json')) {
|
||||
$result = LoginDiagnosticHelper::diagnose(
|
||||
$email !== '' ? $email : null,
|
||||
$mobile !== '' ? $mobile : null
|
||||
);
|
||||
return $this->response->setJSON($result);
|
||||
}
|
||||
|
||||
$query = [];
|
||||
if ($email !== '') {
|
||||
$query['email'] = $email;
|
||||
}
|
||||
if ($mobile !== '') {
|
||||
$query['mobile'] = $mobile;
|
||||
}
|
||||
|
||||
return redirect()->to(base_url('security/login-diagnostic?' . http_build_query($query)));
|
||||
}
|
||||
}
|
||||
@ -853,10 +853,74 @@ class RestAuthenticationController extends AdminController
|
||||
|
||||
if (!empty($HRAccessData) && isset($HRAccessData['allowed_modules'])) {
|
||||
|
||||
$decoded = json_decode($HRAccessData['allowed_modules'], true);
|
||||
// $decoded = json_decode($HRAccessData['allowed_modules'], true);
|
||||
// $getAllhrData[$key]['allowed_modules'] = $decoded;
|
||||
// $HRAccessData['pre_client_id'] = md5($HRAccessData['pre_client_id']);
|
||||
// $HRAccessData['post_client_id'] = md5($HRAccessData['post_client_id']);
|
||||
|
||||
$decoded = json_decode($HRAccessData['allowed_modules'], true) ?? [];
|
||||
$allowedActivePolicies = json_decode($HRAccessData['allowed_active_policies'] ?? '[]', true) ?? [];
|
||||
if (!is_array($allowedActivePolicies)) {
|
||||
$allowedActivePolicies = [];
|
||||
}
|
||||
|
||||
if (empty($allowedActivePolicies) && is_array($decoded)) {
|
||||
if (isset($decoded['post']) && is_array($decoded['post'])) {
|
||||
$decoded['post'] = array_values(array_filter(
|
||||
$decoded['post'],
|
||||
static fn($module) => (int) $module !== 4
|
||||
));
|
||||
} elseif (!isset($decoded['pre']) && !isset($decoded['post'])) {
|
||||
$decoded = array_values(array_filter(
|
||||
$decoded,
|
||||
static fn($module) => (int) $module !== 4
|
||||
));
|
||||
}
|
||||
$HRAccessData['allowed_modules'] = json_encode($decoded);
|
||||
}
|
||||
|
||||
$claimsSubMenu = [];
|
||||
$hasClaimsModule = false;
|
||||
if (is_array($decoded)) {
|
||||
if (isset($decoded['post']) && is_array($decoded['post'])) {
|
||||
$hasClaimsModule = in_array(4, array_map('intval', $decoded['post']), true);
|
||||
} elseif (!isset($decoded['pre']) && !isset($decoded['post'])) {
|
||||
$hasClaimsModule = in_array(4, array_map('intval', $decoded), true);
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasClaimsModule) {
|
||||
$claimsSubMenu[] = 'EB';
|
||||
|
||||
$hasNonEbPolicy = false;
|
||||
if (!empty($allowedActivePolicies)) {
|
||||
|
||||
$policyAllocgRows = $db2->table('client_policy')
|
||||
->select('policy_type.allocg')
|
||||
->join('policy_type', 'policy_type.id = client_policy.policy_type_id', 'left')
|
||||
->whereIn('client_policy.id', $allowedActivePolicies)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($policyAllocgRows as $policyRow) {
|
||||
if (($policyRow['allocg'] ?? '') !== 'EB') {
|
||||
$hasNonEbPolicy = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasNonEbPolicy) {
|
||||
$claimsSubMenu[] = 'Non-EB';
|
||||
}
|
||||
}
|
||||
|
||||
$getAllhrData[$key]['allowed_modules'] = $decoded;
|
||||
$getAllhrData[$key]['claims_sub_menu'] = $claimsSubMenu;
|
||||
$HRAccessData['pre_client_id'] = md5($HRAccessData['pre_client_id']);
|
||||
$HRAccessData['post_client_id'] = md5($HRAccessData['post_client_id']);
|
||||
$HRAccessData['claims_sub_menu'] = $claimsSubMenu;
|
||||
|
||||
|
||||
$token = JWTToken::encode($HRAccessData);
|
||||
$getAllhrData[$key]['token'] = $token;
|
||||
|
||||
1211
app/Helpers/LoginDiagnosticHelper.php
Normal file
1211
app/Helpers/LoginDiagnosticHelper.php
Normal file
File diff suppressed because it is too large
Load Diff
632
app/Views/admin/login_diagnostic.php
Normal file
632
app/Views/admin/login_diagnostic.php
Normal file
@ -0,0 +1,632 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= esc($title ?? 'Employee Login Diagnostic') ?></title>
|
||||
<style>
|
||||
:root{
|
||||
--bg: #F6F8F9;
|
||||
--surface: #FFFFFF;
|
||||
--border: #E3E7EA;
|
||||
--border-strong: #CBD2D8;
|
||||
--text-primary: #16212B;
|
||||
--text-secondary: #5B6770;
|
||||
--text-muted: #8B959C;
|
||||
--teal: #0E8F9C;
|
||||
--teal-dark: #0A6B75;
|
||||
--teal-tint: #E7F5F6;
|
||||
--success-bg: #E4F5EC;
|
||||
--success-border: #B9E3CC;
|
||||
--success-text: #1B7A4C;
|
||||
--warn-bg: #FFF6E0;
|
||||
--warn-border: #F0D9A0;
|
||||
--warn-text: #8A5B00;
|
||||
--danger-bg: #FDECEE;
|
||||
--danger-border: #F0C4CB;
|
||||
--danger-text: #9B1C3A;
|
||||
--danger-code: #C43D6E;
|
||||
--low-badge-bg: #EDEFF1;
|
||||
--low-badge-text: #4A545C;
|
||||
--med-badge-bg: #FFF3CD;
|
||||
--med-badge-text: #664D03;
|
||||
--high-badge-bg: #F8D7DA;
|
||||
--high-badge-text: #842029;
|
||||
--mono: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace;
|
||||
--sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
}
|
||||
*{box-sizing:border-box;}
|
||||
body{
|
||||
margin:0;
|
||||
background:var(--bg);
|
||||
font-family:var(--sans);
|
||||
color:var(--text-primary);
|
||||
padding:32px 40px 64px;
|
||||
}
|
||||
.wrap{max-width:1320px;margin:0 auto;}
|
||||
|
||||
.page-head{margin-bottom:24px;}
|
||||
.page-head h1{font-size:22px;font-weight:600;margin:0 0 4px;letter-spacing:-0.01em;}
|
||||
.page-head p{margin:0;font-size:13.5px;color:var(--text-secondary);}
|
||||
.page-head p b{color:var(--text-primary);font-weight:600;}
|
||||
|
||||
.card{
|
||||
background:var(--surface);
|
||||
border:1px solid var(--border);
|
||||
border-radius:var(--radius-md);
|
||||
}
|
||||
|
||||
.search-card{padding:20px 24px;margin-bottom:16px;}
|
||||
.search-grid{display:grid;grid-template-columns:1fr 1fr auto auto;gap:16px;align-items:end;}
|
||||
.field label{display:block;font-size:12.5px;font-weight:600;color:var(--text-primary);margin-bottom:6px;}
|
||||
.field label span{font-weight:400;color:var(--text-muted);font-family:var(--mono);font-size:11.5px;margin-left:2px;}
|
||||
.field input{
|
||||
width:100%;height:38px;border:1px solid var(--border-strong);border-radius:var(--radius-sm);
|
||||
padding:0 12px;font-size:13.5px;font-family:var(--sans);color:var(--text-primary);
|
||||
background:#FCFDFD;
|
||||
}
|
||||
.field input::placeholder{color:var(--text-muted);}
|
||||
.field input:focus{outline:none;border-color:var(--teal);box-shadow:0 0 0 3px rgba(14,143,156,0.12);}
|
||||
.btn{
|
||||
height:38px;padding:0 20px;border-radius:var(--radius-sm);font-size:13.5px;font-weight:600;
|
||||
border:1px solid transparent;cursor:pointer;white-space:nowrap;text-decoration:none;
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
}
|
||||
.btn-primary{background:var(--teal);color:#fff;}
|
||||
.btn-primary:hover{background:var(--teal-dark);}
|
||||
.btn-ghost{background:#fff;border-color:var(--border-strong);color:var(--text-primary);}
|
||||
.btn-ghost:hover{background:#F5F6F7;}
|
||||
.search-hint{grid-column:1/-1;font-size:12px;color:var(--text-muted);margin:2px 0 0;}
|
||||
.search-hint b{color:var(--text-secondary);font-weight:600;}
|
||||
|
||||
.status-banner{
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
border-radius:var(--radius-md);padding:16px 22px;margin-bottom:16px;
|
||||
}
|
||||
.status-ok{background:var(--success-bg);border:1px solid var(--success-border);}
|
||||
.status-ok .txt strong{color:var(--success-text);}
|
||||
.status-ok .txt span{color:#2C5F45;}
|
||||
.status-ok .copy-btn{border-color:var(--success-border);color:var(--success-text);}
|
||||
.status-ok .copy-btn:hover{background:#F1FBF6;}
|
||||
.status-warn,.status-partial{background:var(--warn-bg);border:1px solid var(--warn-border);}
|
||||
.status-warn .txt strong,.status-partial .txt strong{color:var(--warn-text);}
|
||||
.status-warn .txt span,.status-partial .txt span{color:#6B4A10;}
|
||||
.status-warn .copy-btn,.status-partial .copy-btn{border-color:var(--warn-border);color:var(--warn-text);}
|
||||
.status-fail{background:var(--danger-bg);border:1px solid var(--danger-border);}
|
||||
.status-fail .txt strong{color:var(--danger-text);}
|
||||
.status-fail .txt span{color:#7A2A3A;}
|
||||
.status-fail .copy-btn{border-color:var(--danger-border);color:var(--danger-text);}
|
||||
.status-banner .txt strong{display:block;font-size:14.5px;font-weight:700;margin-bottom:2px;}
|
||||
.status-banner .txt span{font-size:13px;}
|
||||
.status-banner .copy-btn{
|
||||
background:#fff;font-size:12.5px;font-weight:600;padding:8px 14px;border-radius:var(--radius-sm);
|
||||
cursor:pointer;border:1px solid;
|
||||
}
|
||||
|
||||
.section-title{
|
||||
font-size:12.5px;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;
|
||||
color:var(--text-secondary);padding:14px 20px;background:#F8F9FA;
|
||||
border:1px solid var(--border);border-bottom:none;border-radius:var(--radius-md) var(--radius-md) 0 0;
|
||||
}
|
||||
.section-title span{text-transform:none;font-weight:400;letter-spacing:0;color:var(--text-muted);}
|
||||
table{width:100%;border-collapse:collapse;}
|
||||
.problems-table{border:1px solid var(--border);border-radius:0 0 var(--radius-md) var(--radius-md);overflow:hidden;margin-bottom:20px;background:#fff;}
|
||||
.problems-table th{
|
||||
text-align:left;font-size:12px;font-weight:700;color:var(--text-secondary);
|
||||
padding:10px 20px;border-bottom:1px solid var(--border);background:#fff;
|
||||
}
|
||||
.problems-table td{
|
||||
padding:12px 20px;font-size:13.5px;border-bottom:1px solid var(--border);
|
||||
border-left:3px solid var(--teal);vertical-align:middle;
|
||||
}
|
||||
.problems-table tr:last-child td{border-bottom:none;}
|
||||
.problems-table tr:nth-child(even) td{background:#FAFBFC;}
|
||||
.problems-table .empty{border-left-color:transparent;color:var(--text-muted);text-align:center;}
|
||||
.badge{
|
||||
display:inline-block;font-size:11px;font-weight:700;padding:3px 9px;border-radius:4px;letter-spacing:0.03em;
|
||||
}
|
||||
.badge-low{background:var(--low-badge-bg);color:var(--low-badge-text);}
|
||||
.badge-medium{background:var(--med-badge-bg);color:var(--med-badge-text);}
|
||||
.badge-high{background:var(--high-badge-bg);color:var(--high-badge-text);}
|
||||
.code{font-family:var(--mono);font-size:12.5px;color:var(--danger-code);font-weight:600;}
|
||||
.fix-text{color:var(--text-secondary);}
|
||||
|
||||
.compare-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;}
|
||||
.compare-card{border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden;background:#fff;}
|
||||
.compare-head{
|
||||
padding:14px 20px;background:#F8F9FA;border-bottom:1px solid var(--border);
|
||||
font-size:13px;font-weight:700;color:var(--text-primary);
|
||||
}
|
||||
.compare-head span{font-weight:400;color:var(--text-muted);}
|
||||
.kv-table td{padding:10px 20px;font-size:13px;border-bottom:1px solid var(--border);}
|
||||
.kv-table tr:last-child td{border-bottom:none;}
|
||||
.kv-table tr:nth-child(even) td{background:#FAFBFC;}
|
||||
.kv-key{color:var(--text-secondary);font-weight:600;width:44%;}
|
||||
.kv-val{color:var(--text-primary);font-family:var(--mono);font-size:12.5px;}
|
||||
.check{color:#1B9C56;font-weight:700;}
|
||||
.cross{color:var(--danger-code);font-weight:700;}
|
||||
.val-flag{color:var(--danger-code);font-weight:700;}
|
||||
.alert-inline{margin:12px 20px;padding:10px 12px;border-radius:var(--radius-sm);background:var(--warn-bg);border:1px solid var(--warn-border);color:var(--warn-text);font-size:12.5px;}
|
||||
|
||||
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--border);margin-bottom:0;padding:0 4px;}
|
||||
.tab{
|
||||
padding:11px 18px;font-size:13px;font-weight:600;color:var(--text-secondary);
|
||||
cursor:pointer;border-bottom:2px solid transparent;background:none;border-top:none;border-left:none;border-right:none;
|
||||
font-family:var(--sans);
|
||||
}
|
||||
.tab.active{color:var(--teal-dark);border-bottom-color:var(--teal);}
|
||||
.tab:hover:not(.active){color:var(--text-primary);}
|
||||
.tab-panel{border:1px solid var(--border);border-top:none;border-radius:0 0 var(--radius-md) var(--radius-md);overflow:hidden;background:#fff;display:none;}
|
||||
.tab-panel.active{display:block;}
|
||||
.cred-table th{
|
||||
text-align:left;font-size:12px;font-weight:700;color:var(--text-secondary);
|
||||
padding:12px 20px;background:#F8F9FA;border-bottom:1px solid var(--border);
|
||||
}
|
||||
.cred-table th.center, .cred-table td.center{text-align:left;width:22%;}
|
||||
.cred-table td{padding:11px 20px;font-size:13.5px;border-bottom:1px solid var(--border);}
|
||||
.cred-table tr:last-child td{border-bottom:none;}
|
||||
.cred-table tr:nth-child(even) td{background:#FAFBFC;}
|
||||
.cred-key{font-weight:600;color:var(--text-primary);}
|
||||
.val-yes{color:#1B9C56;font-weight:600;}
|
||||
.val-no{color:var(--text-muted);}
|
||||
.val-raw{font-family:var(--mono);color:var(--danger-code);font-weight:600;}
|
||||
.muted{color:var(--text-muted);font-size:13px;padding:16px 20px;}
|
||||
.dup-wrap{padding:16px 20px;}
|
||||
.dup-wrap h6{margin:0 0 10px;font-size:13px;font-weight:700;}
|
||||
.dup-wrap h6 + .dup-table, .dup-wrap .dup-block{margin-bottom:18px;}
|
||||
.dup-subhead{font-size:12px;font-weight:700;color:var(--text-secondary);margin:14px 0 8px;text-transform:uppercase;letter-spacing:0.03em;}
|
||||
.dup-subhead .count{font-weight:600;color:var(--text-muted);text-transform:none;letter-spacing:0;}
|
||||
.dup-table{border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;}
|
||||
.dup-table th,.dup-table td{padding:8px 12px;font-size:12px;border-bottom:1px solid var(--border);text-align:left;}
|
||||
.dup-table th{background:#F8F9FA;font-weight:700;color:var(--text-secondary);}
|
||||
.dup-table tr:last-child td{border-bottom:none;}
|
||||
.winner{display:inline-block;background:var(--success-bg);color:var(--success-text);font-size:10px;font-weight:700;padding:2px 6px;border-radius:4px;margin-right:4px;}
|
||||
.dup-badge{display:inline-block;font-size:10px;font-weight:700;padding:2px 6px;border-radius:4px;}
|
||||
.dup-self{background:var(--danger-bg);color:var(--danger-text);}
|
||||
.dup-family{background:var(--warn-bg);color:var(--warn-text);}
|
||||
.row-self-dup td{background:#FDECEE !important;}
|
||||
.row-family-dup td{background:#FFF8E8 !important;}
|
||||
.problems-table tr.sev-high td{border-left-color:var(--danger-text);background:#FDF2F4;}
|
||||
.problems-table tr.sev-medium td{border-left-color:#C9A227;}
|
||||
.problems-table tr.sev-low td{border-left-color:var(--text-muted);}
|
||||
.window-open{color:#1B9C56;font-weight:700;}
|
||||
.window-closed{color:var(--danger-text);font-weight:700;}
|
||||
.window-pending{color:var(--warn-text);font-weight:700;}
|
||||
.window-missing{color:var(--text-muted);font-weight:600;}
|
||||
.policy-wrap{padding:16px 20px;}
|
||||
.policy-wrap h6{margin:0 0 10px;font-size:13px;font-weight:700;}
|
||||
.policy-note{font-size:12px;color:var(--text-muted);margin:0 0 12px;}
|
||||
|
||||
@media (max-width:900px){
|
||||
body{padding:20px 16px 40px;}
|
||||
.search-grid{grid-template-columns:1fr;}
|
||||
.compare-grid{grid-template-columns:1fr;}
|
||||
.status-banner{flex-direction:column;align-items:flex-start;gap:12px;}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<div class="page-head">
|
||||
<h1>Employee login diagnostic</h1>
|
||||
<p><b>PRE</b> (enrollment) + <b>POST</b> (live) · read-only · does not send OTP</p>
|
||||
</div>
|
||||
|
||||
<form method="get" action="<?= base_url('security/login-diagnostic') ?>" class="card search-card">
|
||||
<div class="search-grid">
|
||||
<div class="field">
|
||||
<label>Email <span>email_corporate</span></label>
|
||||
<input type="text" name="email" value="<?= esc($email ?? '') ?>" placeholder="user@company.com">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Mobile</label>
|
||||
<input type="text" name="mobile" value="<?= esc($mobile ?? '') ?>" placeholder="10-digit mobile">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Diagnose</button>
|
||||
<a class="btn btn-ghost" href="<?= base_url('security/login-diagnostic') ?>">Clear</a>
|
||||
<p class="search-hint">If both are filled, login logic uses <b>mobile first</b> (same as auth APIs).</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if (! empty($has_search) && is_array($result ?? null)): ?>
|
||||
<?php
|
||||
$v = $result['verdict'] ?? 'WILL_FAIL';
|
||||
$bannerClass = match ($v) {
|
||||
'OK' => 'status-ok',
|
||||
'OK_WITH_WARNINGS' => 'status-warn',
|
||||
'PARTIAL' => 'status-partial',
|
||||
default => 'status-fail',
|
||||
};
|
||||
$pre = $result['pre'] ?? [];
|
||||
$post = $result['post'] ?? [];
|
||||
$merge = $result['merge'] ?? [];
|
||||
$problems = $result['problems'] ?? [];
|
||||
$connectivity = $result['connectivity'] ?? [];
|
||||
$pc = $pre['credentials'] ?? [];
|
||||
$oc = $post['credentials'] ?? [];
|
||||
$pt = $pre['token'] ?? [];
|
||||
$ot = $post['token'] ?? [];
|
||||
$yn = static fn ($ok) => $ok ? '<span class="val-yes">Yes</span>' : '<span class="val-no">No</span>';
|
||||
$rawFlag = static function ($val): string {
|
||||
if ($val === null || $val === '') {
|
||||
return 'null';
|
||||
}
|
||||
return (string) $val;
|
||||
};
|
||||
$mark = static fn (bool $ok) => $ok ? '<span class="check">✓</span>' : '<span class="cross">✗</span>';
|
||||
?>
|
||||
|
||||
<div class="status-banner <?= esc($bannerClass) ?>">
|
||||
<div class="txt">
|
||||
<strong><?= esc($v) ?></strong>
|
||||
<span><?= esc($result['verdict_message'] ?? '') ?></span>
|
||||
</div>
|
||||
<button type="button" class="copy-btn" id="copyDiagSummary">Copy summary</button>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Problems <span>(failed checks only)</span></div>
|
||||
<table class="problems-table">
|
||||
<tr>
|
||||
<th style="width:80px;">Severity</th>
|
||||
<th style="width:190px;">Code</th>
|
||||
<th>Message</th>
|
||||
<th style="width:340px;">Fix</th>
|
||||
</tr>
|
||||
<?php if (empty($problems)): ?>
|
||||
<tr><td class="empty" colspan="4">No problems detected.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($problems as $p): ?>
|
||||
<?php $sev = strtolower((string) ($p['severity'] ?? 'low')); ?>
|
||||
<tr class="sev-<?= esc($sev) ?>">
|
||||
<td><span class="badge badge-<?= esc($sev) ?>"><?= esc(strtoupper($sev)) ?></span></td>
|
||||
<td><span class="code"><?= esc($p['code'] ?? '') ?></span></td>
|
||||
<td><?= esc($p['message'] ?? '') ?></td>
|
||||
<td class="fix-text"><?= esc($p['fix'] ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
|
||||
<div class="compare-grid">
|
||||
<div class="compare-card">
|
||||
<div class="compare-head">PRE <span>(enrollment)</span></div>
|
||||
<?= view('admin/login_diagnostic_side', ['side' => $pre, 'mark' => $mark]) ?>
|
||||
</div>
|
||||
<div class="compare-card">
|
||||
<div class="compare-head">POST <span>(live)</span></div>
|
||||
<?= view('admin/login_diagnostic_side', ['side' => $post, 'mark' => $mark]) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs" role="tablist">
|
||||
<button type="button" class="tab active" data-tab="merge">Merge</button>
|
||||
<button type="button" class="tab" data-tab="dup">Duplicates</button>
|
||||
<button type="button" class="tab" data-tab="policies">Policies</button>
|
||||
<button type="button" class="tab" data-tab="wellness">Wellness</button>
|
||||
<button type="button" class="tab" data-tab="cred">Credentials</button>
|
||||
<button type="button" class="tab" data-tab="token">Token</button>
|
||||
<button type="button" class="tab" data-tab="conn">Connectivity</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel active" id="panel-merge">
|
||||
<table class="cred-table">
|
||||
<tr><td class="cred-key">PRE eligible</td><td><?= ! empty($merge['pre_eligible']) ? 'Yes' : 'No' ?></td></tr>
|
||||
<tr><td class="cred-key">POST eligible</td><td><?= ! empty($merge['post_eligible']) ? 'Yes' : 'No' ?></td></tr>
|
||||
<tr><td class="cred-key">PRE short_name</td><td class="val-raw"><?= esc((string) ($merge['pre_short_name'] ?? '')) ?></td></tr>
|
||||
<tr><td class="cred-key">POST short_name</td><td class="val-raw"><?= esc((string) ($merge['post_short_name'] ?? '')) ?></td></tr>
|
||||
<tr><td class="cred-key">short_name match</td><td><?= isset($merge['short_name_match']) ? ($merge['short_name_match'] ? 'Yes' : 'No') : '—' ?></td></tr>
|
||||
<tr><td class="cred-key">PRE created_at</td><td class="val-raw"><?= esc((string) ($merge['pre_created_at'] ?? '—')) ?></td></tr>
|
||||
<tr><td class="cred-key">POST created_at</td><td class="val-raw"><?= esc((string) ($merge['post_created_at'] ?? '—')) ?></td></tr>
|
||||
<tr><td class="cred-key">Would return both</td><td><?= ! empty($merge['would_return_both']) ? 'Yes' : 'No' ?></td></tr>
|
||||
<tr><td class="cred-key">Winner</td><td><b><?= esc((string) ($merge['winner'] ?? '—')) ?></b></td></tr>
|
||||
<tr><td class="cred-key">OTP sync possible</td><td><?= ! empty($merge['otp_sync_possible']) ? 'Yes' : 'No' ?></td></tr>
|
||||
<tr><td class="cred-key">Reason</td><td class="fix-text"><?= esc((string) ($merge['reason'] ?? '')) ?></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="panel-dup">
|
||||
<div class="dup-wrap">
|
||||
<?php
|
||||
$preDup = $pre['duplicates'] ?? [];
|
||||
$postDup = $post['duplicates'] ?? [];
|
||||
$renderSideDups = static function (string $title, array $side, array $dup): void {
|
||||
$selfRows = $dup['self_rows'] ?? [];
|
||||
$familyRows = $dup['family_rows'] ?? [];
|
||||
// Fallback if older payload
|
||||
if ($selfRows === [] && $familyRows === [] && ! empty($side['raw_matches'])) {
|
||||
foreach ($side['raw_matches'] as $r) {
|
||||
if (strtolower(trim((string) ($r['relationship'] ?? ''))) === 'self') {
|
||||
$selfRows[] = $r;
|
||||
} else {
|
||||
$familyRows[] = $r;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="dup-block">
|
||||
<h6><?= esc($title) ?></h6>
|
||||
<div class="dup-subhead">Self <span class="count">(<?= count($selfRows) ?>)</span></div>
|
||||
<?= view('admin/login_diagnostic_matches', ['rows' => $selfRows]) ?>
|
||||
<div class="dup-subhead">Family <span class="count">(<?= count($familyRows) ?>)</span></div>
|
||||
<?= view('admin/login_diagnostic_matches', ['rows' => $familyRows]) ?>
|
||||
</div>
|
||||
<?php
|
||||
};
|
||||
?>
|
||||
<?php $renderSideDups('PRE raw matches', $pre, $preDup); ?>
|
||||
<?php if (! empty($pre['raw_by_personal'])): ?>
|
||||
<div class="dup-block">
|
||||
<h6>PRE matches on email_personal only</h6>
|
||||
<?= view('admin/login_diagnostic_matches', ['rows' => $pre['raw_by_personal']]) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php $renderSideDups('POST raw matches', $post, $postDup); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="panel-policies">
|
||||
<div class="policy-wrap">
|
||||
<p class="policy-note">PRE: enrollment open/close on <code>employee_polices</code>. POST: live policy mapping. Wellness details are on the <b>Wellness</b> tab (POST only).</p>
|
||||
<?php if (! empty($pre['policies_error'])): ?>
|
||||
<div class="alert-inline">PRE policies error: <?= esc($pre['policies_error']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($post['policies_error'])): ?>
|
||||
<div class="alert-inline">POST policies error: <?= esc($post['policies_error']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$windowClass = static function (string $w): string {
|
||||
return match ($w) {
|
||||
'OPEN' => 'window-open',
|
||||
'CLOSED' => 'window-closed',
|
||||
'NOT_STARTED' => 'window-pending',
|
||||
default => 'window-missing',
|
||||
};
|
||||
};
|
||||
$renderPolicies = static function (string $title, array $policies, bool $showEnrollment) use ($windowClass): void {
|
||||
?>
|
||||
<div class="dup-block">
|
||||
<h6><?= esc($title) ?> <span class="count">(<?= count($policies) ?>)</span></h6>
|
||||
<?php if (empty($policies)): ?>
|
||||
<p class="muted" style="padding:0;">None</p>
|
||||
<?php else: ?>
|
||||
<div class="dup-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Emp policy</th>
|
||||
<th>Client policy</th>
|
||||
<th>Policy no</th>
|
||||
<th>EP status</th>
|
||||
<th>EP active</th>
|
||||
<th>Policy period</th>
|
||||
<?php if ($showEnrollment): ?>
|
||||
<th>Enrollment open</th>
|
||||
<th>Enrollment close</th>
|
||||
<th>Window</th>
|
||||
<?php else: ?>
|
||||
<th>CP open_for_enrollment</th>
|
||||
<th>CP open/close</th>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($policies as $pol): ?>
|
||||
<?php
|
||||
$w = (string) ($pol['enrollment_window'] ?? '');
|
||||
$rowClass = ($showEnrollment && in_array($w, ['CLOSED', 'NOT_STARTED', 'MISSING'], true)) ? 'row-family-dup' : '';
|
||||
if ($showEnrollment && $w === 'CLOSED') {
|
||||
$rowClass = 'row-self-dup';
|
||||
}
|
||||
?>
|
||||
<tr class="<?= esc($rowClass) ?>">
|
||||
<td><?= esc((string) ($pol['emp_policy_id'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($pol['client_policy_id'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($pol['policy_no'] ?? '—')) ?></td>
|
||||
<td><?= esc((string) ($pol['emp_policy_status'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($pol['emp_policy_is_active'] ?? '')) ?></td>
|
||||
<td><?= esc((string) (($pol['policy_start_date'] ?? '—') . ' → ' . ($pol['policy_end_date'] ?? '—'))) ?></td>
|
||||
<?php if ($showEnrollment): ?>
|
||||
<td class="val-raw"><?= esc((string) ($pol['enrollment_open_date'] ?? 'null')) ?></td>
|
||||
<td class="val-raw"><?= esc((string) ($pol['enrollment_close_date'] ?? 'null')) ?></td>
|
||||
<td class="<?= esc($windowClass($w)) ?>"><?= esc($w) ?></td>
|
||||
<?php else: ?>
|
||||
<td><?= esc((string) ($pol['open_for_enrollment'] ?? '—')) ?></td>
|
||||
<td><?= esc((string) (($pol['client_open_date'] ?? '—') . ' → ' . ($pol['client_close_date'] ?? '—'))) ?></td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
};
|
||||
$renderPolicies('PRE employee policies (enrollment dates)', $pre['policies'] ?? [], true);
|
||||
$renderPolicies('POST employee policies (live mapping)', $post['policies'] ?? [], false);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="panel-wellness">
|
||||
<div class="policy-wrap">
|
||||
<p class="policy-note"><b>POST only.</b> Wellness SSO needs <code>wellness_onboard</code> not null and not <code>0</code> (same as <code>getWellnessUrl</code>). PRE has no wellness fields.</p>
|
||||
<?php
|
||||
$postPolicies = $post['policies'] ?? [];
|
||||
$wellnessReadyCount = 0;
|
||||
$wellnessPlanCount = 0;
|
||||
foreach ($postPolicies as $wp) {
|
||||
if (! empty($wp['wellness_ready'])) {
|
||||
$wellnessReadyCount++;
|
||||
}
|
||||
$pid = $wp['wellness_plan_id'] ?? null;
|
||||
if ($pid !== null && $pid !== '' && (string) $pid !== '0') {
|
||||
$wellnessPlanCount++;
|
||||
}
|
||||
}
|
||||
$overallReady = $wellnessReadyCount > 0;
|
||||
?>
|
||||
<table class="cred-table" style="margin-bottom:16px;">
|
||||
<tr>
|
||||
<td class="cred-key">Overall wellness ready</td>
|
||||
<td class="<?= $overallReady ? 'window-open' : 'window-closed' ?>">
|
||||
<?= $overallReady ? 'Yes' : 'No' ?>
|
||||
<span class="fix-text">(<?= (int) $wellnessReadyCount ?> of <?= count($postPolicies) ?> policies onboarded)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="cred-key">Policies with wellness_plan_id</td>
|
||||
<td><?= (int) $wellnessPlanCount ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="cred-key">Rule</td>
|
||||
<td class="fix-text">Ready = <code>wellness_onboard</code> is not null and not 0</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="dup-block">
|
||||
<h6>POST wellness by policy <span class="count">(<?= count($postPolicies) ?>)</span></h6>
|
||||
<?php if (empty($postPolicies)): ?>
|
||||
<p class="muted" style="padding:0;">No POST employee policies to check.</p>
|
||||
<?php else: ?>
|
||||
<div class="dup-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Emp policy</th>
|
||||
<th>Client policy</th>
|
||||
<th>Policy no</th>
|
||||
<th>EP status</th>
|
||||
<th>EP active</th>
|
||||
<th>wellness_onboard</th>
|
||||
<th>Wellness ready</th>
|
||||
<th>wellness_plan_id</th>
|
||||
<th>wellness_vendor_id</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($postPolicies as $pol): ?>
|
||||
<?php $ready = ! empty($pol['wellness_ready']); ?>
|
||||
<tr class="<?= $ready ? '' : 'row-self-dup' ?>">
|
||||
<td><?= esc((string) ($pol['emp_policy_id'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($pol['client_policy_id'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($pol['policy_no'] ?? '—')) ?></td>
|
||||
<td><?= esc((string) ($pol['emp_policy_status'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($pol['emp_policy_is_active'] ?? '')) ?></td>
|
||||
<td class="val-raw"><?= esc((string) ($pol['wellness_onboard_raw'] ?? 'null')) ?></td>
|
||||
<td class="<?= $ready ? 'window-open' : 'window-closed' ?>"><?= $ready ? 'Yes' : 'No' ?></td>
|
||||
<td class="val-raw"><?= esc((string) (($pol['wellness_plan_id'] ?? '') !== '' && ($pol['wellness_plan_id'] ?? null) !== null ? $pol['wellness_plan_id'] : 'null')) ?></td>
|
||||
<td class="val-raw"><?= esc((string) (($pol['wellness_vendor_id'] ?? '') !== '' && ($pol['wellness_vendor_id'] ?? null) !== null ? $pol['wellness_vendor_id'] : 'null')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="panel-cred">
|
||||
<table class="cred-table">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="center">PRE</th>
|
||||
<th class="center">POST</th>
|
||||
</tr>
|
||||
<tr><td class="cred-key">OTP set</td><td class="center"><?= $yn(! empty($pc['otp_set'])) ?></td><td class="center"><?= $yn(! empty($oc['otp_set'])) ?></td></tr>
|
||||
<tr><td class="cred-key">MPIN set</td><td class="center"><?= $yn(! empty($pc['mpin_set'])) ?></td><td class="center"><?= $yn(! empty($oc['mpin_set'])) ?></td></tr>
|
||||
<tr><td class="cred-key">MPIN skipped</td><td class="center"><?= $yn(! empty($pc['mpin_skipped'])) ?></td><td class="center"><?= $yn(! empty($oc['mpin_skipped'])) ?></td></tr>
|
||||
<tr><td class="cred-key">Biometric set</td><td class="center"><?= $yn(! empty($pc['biometric_set'])) ?></td><td class="center"><?= $yn(! empty($oc['biometric_set'])) ?></td></tr>
|
||||
<tr>
|
||||
<td class="cred-key">is_biometric_enabled <span style="font-weight:400;color:var(--text-muted);">(raw)</span></td>
|
||||
<td class="center val-raw"><?= esc($rawFlag($pc['is_biometric_enabled'] ?? null)) ?></td>
|
||||
<td class="center val-raw"><?= esc($rawFlag($oc['is_biometric_enabled'] ?? null)) ?></td>
|
||||
</tr>
|
||||
<tr><td class="cred-key">Password set</td><td class="center"><?= $yn(! empty($pc['password_set'])) ?></td><td class="center"><?= $yn(! empty($oc['password_set'])) ?></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="panel-token">
|
||||
<table class="cred-table">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="center">PRE</th>
|
||||
<th class="center">POST</th>
|
||||
</tr>
|
||||
<tr><td class="cred-key">Status</td><td class="center"><?= esc((string) ($pt['status'] ?? '—')) ?></td><td class="center"><?= esc((string) ($ot['status'] ?? '—')) ?></td></tr>
|
||||
<tr><td class="cred-key">Details</td><td class="center fix-text"><?= esc((string) ($pt['human'] ?? '—')) ?></td><td class="center fix-text"><?= esc((string) ($ot['human'] ?? '—')) ?></td></tr>
|
||||
<tr><td class="cred-key">TOKENTIMEOUT env</td><td colspan="2"><?= esc((string) ($pt['tokentimeout_env'] ?? $ot['tokentimeout_env'] ?? '—')) ?> seconds</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="panel-conn">
|
||||
<table class="cred-table">
|
||||
<tr><td class="cred-key">POST_ENROLLMENT_BASEURL</td><td class="val-raw"><?= esc((string) ($connectivity['base_url'] ?? '')) ?></td></tr>
|
||||
<tr><td class="cred-key">App-Signature configured</td><td><?= ! empty($connectivity['app_signature']) ? 'Yes' : 'No' ?></td></tr>
|
||||
<tr><td class="cred-key">POST API reachable</td><td><?= ! empty($connectivity['api_reachable']) ? 'Yes' : 'No' ?> (HTTP <?= esc((string) ($connectivity['api_http_code'] ?? '—')) ?>)</td></tr>
|
||||
<tr><td class="cred-key">API message</td><td class="fix-text"><?= esc((string) ($connectivity['api_message'] ?? '')) ?></td></tr>
|
||||
<tr><td class="cred-key">postDB connected</td><td><?= ! empty($connectivity['post_db']) ? 'Yes' : 'No' ?></td></tr>
|
||||
<?php if (! empty($connectivity['post_db_message'])): ?>
|
||||
<tr><td class="cred-key">postDB error</td><td class="fix-text"><?= esc((string) $connectivity['post_db_message']) ?></td></tr>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var tabs = document.querySelectorAll('.tab');
|
||||
var panels = {
|
||||
merge: document.getElementById('panel-merge'),
|
||||
dup: document.getElementById('panel-dup'),
|
||||
policies: document.getElementById('panel-policies'),
|
||||
wellness: document.getElementById('panel-wellness'),
|
||||
cred: document.getElementById('panel-cred'),
|
||||
token: document.getElementById('panel-token'),
|
||||
conn: document.getElementById('panel-conn')
|
||||
};
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
tabs.forEach(function (t) { t.classList.remove('active'); });
|
||||
Object.keys(panels).forEach(function (k) { panels[k].classList.remove('active'); });
|
||||
tab.classList.add('active');
|
||||
var key = tab.getAttribute('data-tab');
|
||||
if (panels[key]) panels[key].classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
var btn = document.getElementById('copyDiagSummary');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var lines = [];
|
||||
lines.push('Verdict: <?= esc($v, 'js') ?>');
|
||||
lines.push('<?= esc($result['verdict_message'] ?? '', 'js') ?>');
|
||||
lines.push('Input email=<?= esc($result['input']['email'] ?? '', 'js') ?> mobile=<?= esc($result['input']['mobile'] ?? '', 'js') ?>');
|
||||
lines.push('Merge winner: <?= esc($merge['winner'] ?? '', 'js') ?>');
|
||||
<?php foreach ($problems as $p): ?>
|
||||
lines.push('[<?= esc(strtoupper((string) ($p['severity'] ?? '')), 'js') ?>] <?= esc($p['code'] ?? '', 'js') ?>: <?= esc($p['message'] ?? '', 'js') ?>');
|
||||
<?php endforeach; ?>
|
||||
navigator.clipboard.writeText(lines.join('\n')).then(function () {
|
||||
btn.textContent = 'Copied';
|
||||
setTimeout(function () { btn.textContent = 'Copy summary'; }, 1500);
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php elseif (! empty($has_search)): ?>
|
||||
<div class="status-banner status-fail">
|
||||
<div class="txt">
|
||||
<strong>WILL_FAIL</strong>
|
||||
<span>No diagnostic result returned.</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
62
app/Views/admin/login_diagnostic_matches.php
Normal file
62
app/Views/admin/login_diagnostic_matches.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php /** @var array $rows */ ?>
|
||||
<?php if (empty($rows)): ?>
|
||||
<p class="muted" style="padding:0;">None</p>
|
||||
<?php else: ?>
|
||||
<div class="dup-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>ID</th>
|
||||
<th>Client</th>
|
||||
<th>Emp code</th>
|
||||
<th>Name</th>
|
||||
<th>Rel</th>
|
||||
<th>Status</th>
|
||||
<th>Active</th>
|
||||
<th>Mobile</th>
|
||||
<th>Email corp</th>
|
||||
<th>Policies</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $row): ?>
|
||||
<?php
|
||||
$rowClass = '';
|
||||
if (! empty($row['is_self_duplicate']) || ! empty($row['is_cross_self_dup'])) {
|
||||
$rowClass = 'row-self-dup';
|
||||
} elseif (! empty($row['is_family_duplicate'])) {
|
||||
$rowClass = 'row-family-dup';
|
||||
}
|
||||
?>
|
||||
<tr class="<?= esc($rowClass) ?>">
|
||||
<td>
|
||||
<?php if (! empty($row['is_login_winner'])): ?>
|
||||
<span class="winner">WINNER</span>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($row['dup_badge'])): ?>
|
||||
<span class="dup-badge <?= ! empty($row['is_self_duplicate']) || ! empty($row['is_cross_self_dup']) ? 'dup-self' : 'dup-family' ?>">
|
||||
<?= esc((string) $row['dup_badge']) ?>
|
||||
</span>
|
||||
<?php elseif (! empty($row['is_login_pick_if_filters_passed'])): ?>
|
||||
<span class="winner" style="background:#EDEFF1;color:#4A545C;">highest id</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= esc((string) ($row['id'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($row['client_id'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($row['emp_code'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($row['name'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($row['relationship'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($row['emp_status'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($row['is_active'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($row['mobile'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($row['email_corporate'] ?? '')) ?></td>
|
||||
<td><?= esc((string) ($row['policies'] ?? '—')) ?></td>
|
||||
<td><?= esc((string) ($row['created_at'] ?? '')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
48
app/Views/admin/login_diagnostic_side.php
Normal file
48
app/Views/admin/login_diagnostic_side.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/** @var array $side */
|
||||
/** @var callable $mark */
|
||||
$fs = $side['filters_summary'] ?? [];
|
||||
$eligible = $side['eligible'] ?? null;
|
||||
$error = $side['error'] ?? null;
|
||||
?>
|
||||
<?php if (! empty($error)): ?>
|
||||
<div class="alert-inline"><?= esc($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<table class="kv-table">
|
||||
<tr>
|
||||
<td class="kv-key">Raw found</td>
|
||||
<td class="kv-val">
|
||||
<?= ! empty($side['raw_matches'])
|
||||
? $mark(true) . ' Yes (' . count($side['raw_matches']) . ')'
|
||||
: $mark(false) . ' No' ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="kv-key">Eligible for login</td>
|
||||
<td class="kv-val"><?= ! empty($eligible) ? $mark(true) . ' Yes' : $mark(false) . ' No' ?></td>
|
||||
</tr>
|
||||
<tr><td class="kv-key">self</td><td class="kv-val"><?= isset($fs['self']) ? $mark((bool) $fs['self']) : '—' ?></td></tr>
|
||||
<tr><td class="kv-key">is_active</td><td class="kv-val"><?= isset($fs['is_active']) ? $mark((bool) $fs['is_active']) : '—' ?></td></tr>
|
||||
<tr><td class="kv-key">emp_status OK</td><td class="kv-val"><?= isset($fs['emp_status_ok']) ? $mark((bool) $fs['emp_status_ok']) : '—' ?></td></tr>
|
||||
<tr><td class="kv-key">has policy</td><td class="kv-val"><?= isset($fs['has_policy']) ? $mark((bool) $fs['has_policy']) : '—' ?></td></tr>
|
||||
<tr><td class="kv-key">policy status OK</td><td class="kv-val"><?= isset($fs['policy_status_ok']) ? $mark((bool) $fs['policy_status_ok']) : '—' ?></td></tr>
|
||||
<tr><td class="kv-key">client_id</td><td class="kv-val"><?= esc((string) ($side['client_id'] ?? ($eligible['client_id'] ?? '—'))) ?></td></tr>
|
||||
<tr><td class="kv-key">short_name</td><td class="kv-val val-flag"><?= esc((string) ($side['client_short_name'] ?? '')) ?></td></tr>
|
||||
<?php if (! empty($eligible)): ?>
|
||||
<tr><td class="kv-key">employee id</td><td class="kv-val"><?= esc((string) ($eligible['id'] ?? '')) ?></td></tr>
|
||||
<tr><td class="kv-key">emp_code</td><td class="kv-val"><?= esc((string) ($eligible['emp_code'] ?? '')) ?></td></tr>
|
||||
<tr><td class="kv-key">name</td><td class="kv-val"><?= esc((string) ($eligible['name'] ?? '')) ?></td></tr>
|
||||
<tr><td class="kv-key">emp_status</td><td class="kv-val"><?= esc((string) ($eligible['emp_status'] ?? '')) ?></td></tr>
|
||||
<tr><td class="kv-key">mobile</td><td class="kv-val"><?= esc((string) ($eligible['mobile'] ?? '')) ?></td></tr>
|
||||
<tr><td class="kv-key">email_corporate</td><td class="kv-val"><?= esc((string) ($eligible['email_corporate'] ?? '')) ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($fs['notes'])): ?>
|
||||
<tr><td class="kv-key">Notes</td><td class="kv-val" style="font-family:var(--sans);color:var(--text-secondary);"><?= esc(implode(' ', $fs['notes'])) ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php if (! empty($side['eligible_via_api']) && empty($eligible)): ?>
|
||||
<tr>
|
||||
<td class="kv-key">API eligible</td>
|
||||
<td class="kv-val"><span class="check">✓</span> via getPostEmployeeDataForAuth (id <?= esc((string) ($side['eligible_via_api']['id'] ?? '')) ?>)</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
40
tests/_support/TestUploadedFile.php
Normal file
40
tests/_support/TestUploadedFile.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
use CodeIgniter\HTTP\Files\UploadedFile;
|
||||
|
||||
/**
|
||||
* UploadedFile stub for CLI tests — bypasses is_uploaded_file() / move_uploaded_file().
|
||||
*/
|
||||
class TestUploadedFile extends UploadedFile
|
||||
{
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->getError() === UPLOAD_ERR_OK && is_file($this->getTempName());
|
||||
}
|
||||
|
||||
public function move(string $targetPath, ?string $name = null, bool $overwrite = false)
|
||||
{
|
||||
$targetPath = rtrim($targetPath, '/') . '/';
|
||||
|
||||
if (!is_dir($targetPath)) {
|
||||
mkdir($targetPath, 0777, true);
|
||||
}
|
||||
|
||||
$name ??= $this->getName();
|
||||
$destination = $targetPath . $name;
|
||||
|
||||
if (!$overwrite && is_file($destination)) {
|
||||
$destination = $targetPath . uniqid('test_', true) . '_' . $name;
|
||||
}
|
||||
|
||||
if (!copy($this->getTempName(), $destination)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@unlink($this->getTempName());
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
139
tests/unit/EmployeesUploadWithEventsTest.php
Normal file
139
tests/unit/EmployeesUploadWithEventsTest.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\FileModel;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use Tests\Support\TestUploadedFile;
|
||||
|
||||
/**
|
||||
* Tests employeesUplodWithEvents enrollment date handling (d-m-Y / d/m/Y) and file upload validation.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class EmployeesUploadWithEventsTest extends CIUnitTestCase
|
||||
{
|
||||
private const SAMPLE_EXCEL = ROOTPATH . 'public/sample_excel/enrollment.xlsx';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
helper('utility');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider enrollmentDateFormatProvider
|
||||
*/
|
||||
public function testChangeDateFormatAcceptsDashAndSlashFormats(
|
||||
string $openDate,
|
||||
string $closeDate,
|
||||
string $expectedOpen,
|
||||
string $expectedClose
|
||||
): void {
|
||||
$this->assertSame($expectedOpen, change_date_format($openDate, null, 'Y-m-d'));
|
||||
$this->assertSame($expectedClose, change_date_format($closeDate, null, 'Y-m-d'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors employeesUplodWithEvents date + files insert payload (EmployeeController.php ~395-403).
|
||||
*
|
||||
* @dataProvider enrollmentDateFormatProvider
|
||||
*/
|
||||
public function testEnrollmentDatesAreConvertedBeforeFilesTableInsert(
|
||||
string $openDate,
|
||||
string $closeDate,
|
||||
string $expectedOpen,
|
||||
string $expectedClose
|
||||
): void {
|
||||
$postData = [
|
||||
'client_id' => 12,
|
||||
'policy_id' => 34,
|
||||
'client_branch_id' => 56,
|
||||
'created_by' => 78,
|
||||
'enrollment_open_date' => $openDate,
|
||||
'enrollment_close_date' => $closeDate,
|
||||
];
|
||||
|
||||
$insertPayload = $this->buildFilesInsertPayload($postData, 'enrollment_test.xlsx', 78);
|
||||
|
||||
$this->assertSame($expectedOpen, $insertPayload['enrollment_open_date']);
|
||||
$this->assertSame($expectedClose, $insertPayload['enrollment_close_date']);
|
||||
$this->assertSame('enrollment', $insertPayload['action']);
|
||||
$this->assertSame('inprogress', $insertPayload['status']);
|
||||
$this->assertSame(12, $insertPayload['client_id']);
|
||||
$this->assertSame(34, $insertPayload['policy_id']);
|
||||
$this->assertSame(56, $insertPayload['client_branch_id']);
|
||||
$this->assertSame(78, $insertPayload['created_by']);
|
||||
$this->assertSame(78, $insertPayload['hr_id']);
|
||||
}
|
||||
|
||||
public function testValidateExcelFileAcceptsSampleEnrollmentUpload(): void
|
||||
{
|
||||
if (!is_file(self::SAMPLE_EXCEL)) {
|
||||
$this->markTestSkipped('Sample enrollment excel not found.');
|
||||
}
|
||||
|
||||
$uploadedFile = $this->createTestUploadedFile(self::SAMPLE_EXCEL);
|
||||
|
||||
$this->assertTrue(validateExcelFile($uploadedFile));
|
||||
$this->assertTrue($uploadedFile->move(WRITEPATH . 'uploads/excel/'));
|
||||
|
||||
$movedPath = WRITEPATH . 'uploads/excel/' . $uploadedFile->getName();
|
||||
$this->assertFileExists($movedPath);
|
||||
@unlink($movedPath);
|
||||
}
|
||||
|
||||
public function testFileModelAllowedFieldsIncludeEnrollmentDates(): void
|
||||
{
|
||||
$reflection = new \ReflectionClass(FileModel::class);
|
||||
$property = $reflection->getProperty('allowedFields');
|
||||
$property->setAccessible(true);
|
||||
$allowedFields = $property->getValue(new FileModel());
|
||||
|
||||
$this->assertContains('enrollment_open_date', $allowedFields);
|
||||
$this->assertContains('enrollment_close_date', $allowedFields);
|
||||
}
|
||||
|
||||
public static function enrollmentDateFormatProvider(): array
|
||||
{
|
||||
return [
|
||||
'd-m-Y format' => ['01-07-2026', '31-07-2026', '2026-07-01', '2026-07-31'],
|
||||
'd/m/Y format' => ['01/07/2026', '31/07/2026', '2026-07-01', '2026-07-31'],
|
||||
'mixed formats' => ['15-08-2026', '30/08/2026', '2026-08-15', '2026-08-30'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildFilesInsertPayload(array $postData, string $filename, int $loggedInUserId): array
|
||||
{
|
||||
return [
|
||||
'file_name' => $filename,
|
||||
'client_id' => $postData['client_id'] ?? null,
|
||||
'policy_id' => $postData['policy_id'] ?? null,
|
||||
'created_by' => $loggedInUserId,
|
||||
'status' => 'inprogress',
|
||||
'action' => 'enrollment',
|
||||
'client_branch_id' => $postData['client_branch_id'] ?? null,
|
||||
'uploaded_by' => 1,
|
||||
'enrollment_open_date' => change_date_format($postData['enrollment_open_date'] ?? null, null, 'Y-m-d'),
|
||||
'enrollment_close_date' => change_date_format($postData['enrollment_close_date'] ?? null, null, 'Y-m-d'),
|
||||
'hr_id' => $postData['created_by'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
private function createTestUploadedFile(string $sourcePath): TestUploadedFile
|
||||
{
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'enroll_test_') . '.xlsx';
|
||||
copy($sourcePath, $tmpPath);
|
||||
|
||||
return new TestUploadedFile(
|
||||
$tmpPath,
|
||||
basename($sourcePath),
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
filesize($tmpPath),
|
||||
UPLOAD_ERR_OK
|
||||
);
|
||||
}
|
||||
}
|
||||
450
tests/unit/GetVerifiedHrDataTest.php
Normal file
450
tests/unit/GetVerifiedHrDataTest.php
Normal file
@ -0,0 +1,450 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Controllers\RestAuthenticationController;
|
||||
use App\Models\AuthHistoryModel;
|
||||
use App\Models\LevelContactModel;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use Config\Database as DatabaseConfig;
|
||||
use Config\Services;
|
||||
use ReflectionClass;
|
||||
use ReflectionProperty;
|
||||
|
||||
class GetVerifiedHrDataTest extends CIUnitTestCase
|
||||
{
|
||||
private string $jwtSecret = 'test-secret-key-for-get-verified-hr-data';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
putenv("JWT_SECRET={$this->jwtSecret}");
|
||||
$_ENV['JWT_SECRET'] = $this->jwtSecret;
|
||||
$_SERVER['JWT_SECRET'] = $this->jwtSecret;
|
||||
|
||||
putenv('POST_ENROLLMENT_BASEURL=http://example.com/');
|
||||
$_ENV['POST_ENROLLMENT_BASEURL'] = 'http://example.com/';
|
||||
|
||||
$this->mockCurlRequest(['ok' => true, 'source' => 'post_enrollment']);
|
||||
$this->mockRateLimiter();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$this->clearInjectedDbConnections();
|
||||
Services::reset(true);
|
||||
|
||||
putenv('JWT_SECRET');
|
||||
putenv('POST_ENROLLMENT_BASEURL');
|
||||
unset(
|
||||
$_ENV['JWT_SECRET'],
|
||||
$_SERVER['JWT_SECRET'],
|
||||
$_ENV['POST_ENROLLMENT_BASEURL']
|
||||
);
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testInvalidOtpReturnsFailed404()
|
||||
{
|
||||
$hrModel = $this->createHrModelStub([
|
||||
'first' => null,
|
||||
'findAll' => [],
|
||||
]);
|
||||
|
||||
$controller = $this->buildController(
|
||||
['email' => 'hr@example.com', 'otp' => '000000'],
|
||||
$hrModel
|
||||
);
|
||||
|
||||
$body = $this->decodeResponse($controller->getVerifiedHrData());
|
||||
|
||||
$this->assertSame('failed', $body['status']);
|
||||
$this->assertSame(404, $body['code']);
|
||||
$this->assertSame('', $body['data']);
|
||||
$this->assertSame(['ok' => true, 'source' => 'post_enrollment'], $body['post_enrollment']);
|
||||
}
|
||||
|
||||
public function testValidOtpWithNoActiveBranchesReturnsFailed404()
|
||||
{
|
||||
$hrModel = $this->createHrModelStub([
|
||||
'first' => ['id' => 5, 'email' => 'hr@example.com', 'mobile' => '9876543210'],
|
||||
'findAll' => [],
|
||||
'update' => true,
|
||||
]);
|
||||
|
||||
$authHistoryModel = $this->createMock(AuthHistoryModel::class);
|
||||
$authHistoryModel->expects($this->once())->method('insert');
|
||||
|
||||
$controller = $this->buildController(
|
||||
['email' => 'hr@example.com', 'otp' => '123456'],
|
||||
$hrModel,
|
||||
$authHistoryModel
|
||||
);
|
||||
|
||||
$body = $this->decodeResponse($controller->getVerifiedHrData());
|
||||
|
||||
$this->assertSame('failed', $body['status']);
|
||||
$this->assertSame(404, $body['code']);
|
||||
$this->assertSame('', $body['data']);
|
||||
}
|
||||
|
||||
public function testValidMobileOtpReturnsSuccessWithClaimsSubMenu()
|
||||
{
|
||||
$branchRow = [
|
||||
'id' => 5,
|
||||
'mobile' => '9876543210',
|
||||
'email' => 'hr@example.com',
|
||||
'client_id' => 42,
|
||||
'client_name' => 'Acme Corp',
|
||||
'short_name' => 'ACME',
|
||||
'client_branch_id' => 7,
|
||||
'branch_name' => 'HQ',
|
||||
'post_branch_id' => 70,
|
||||
];
|
||||
|
||||
$hrModel = $this->createHrModelStub([
|
||||
'first' => ['id' => 5, 'email' => 'hr@example.com', 'mobile' => '9876543210'],
|
||||
'findAll' => [$branchRow],
|
||||
'update' => true,
|
||||
]);
|
||||
|
||||
$authHistoryModel = $this->createMock(AuthHistoryModel::class);
|
||||
$authHistoryModel->expects($this->once())->method('insert');
|
||||
|
||||
$this->injectDbConnection('postDB', $this->createQueryResultStub([
|
||||
'pre_client_id' => 42,
|
||||
'post_client_id' => 420,
|
||||
'pre_branch_id' => 7,
|
||||
'pre_hr_id' => 5,
|
||||
'allowed_modules' => json_encode(['post' => [1, 4, 5]]),
|
||||
'allowed_active_policies' => json_encode([101, 102]),
|
||||
]));
|
||||
|
||||
$this->injectDbConnection('tests', $this->createQueryResultStub(
|
||||
null,
|
||||
[
|
||||
['allocg' => 'EB'],
|
||||
['allocg' => 'GPA'],
|
||||
]
|
||||
));
|
||||
|
||||
$controller = $this->buildController(
|
||||
['mobile_no' => '9876543210', 'otp' => '123456'],
|
||||
$hrModel,
|
||||
$authHistoryModel
|
||||
);
|
||||
|
||||
$body = $this->decodeResponse($controller->getVerifiedHrData());
|
||||
|
||||
$this->assertSame('success', $body['status']);
|
||||
$this->assertSame(200, $body['code']);
|
||||
$this->assertCount(1, $body['data']);
|
||||
$this->assertSame(['post' => [1, 4, 5]], $body['data'][0]['allowed_modules']);
|
||||
$this->assertSame(['EB', 'Non-EB'], $body['data'][0]['claims_sub_menu']);
|
||||
$this->assertNotEmpty($body['data'][0]['token']);
|
||||
$this->assertSame(['ok' => true, 'source' => 'post_enrollment'], $body['post_enrollment']);
|
||||
}
|
||||
|
||||
public function testClaimsModuleWithoutActivePoliciesStripsModuleId4()
|
||||
{
|
||||
$branchRow = [
|
||||
'id' => 5,
|
||||
'mobile' => null,
|
||||
'email' => 'hr@example.com',
|
||||
'client_id' => 42,
|
||||
'client_name' => 'Acme Corp',
|
||||
'short_name' => 'ACME',
|
||||
'client_branch_id' => 7,
|
||||
'branch_name' => 'HQ',
|
||||
'post_branch_id' => 70,
|
||||
];
|
||||
|
||||
$hrModel = $this->createHrModelStub([
|
||||
'first' => ['id' => 5, 'email' => 'hr@example.com', 'mobile' => null],
|
||||
'findAll' => [$branchRow],
|
||||
'update' => true,
|
||||
]);
|
||||
|
||||
$authHistoryModel = $this->createMock(AuthHistoryModel::class);
|
||||
$authHistoryModel->method('insert')->willReturn(1);
|
||||
|
||||
$this->injectDbConnection('postDB', $this->createQueryResultStub([
|
||||
'pre_client_id' => 42,
|
||||
'post_client_id' => 420,
|
||||
'pre_branch_id' => 7,
|
||||
'pre_hr_id' => 5,
|
||||
'allowed_modules' => json_encode(['post' => [1, 4, 5]]),
|
||||
'allowed_active_policies' => json_encode([]),
|
||||
]));
|
||||
|
||||
$controller = $this->buildController(
|
||||
['email' => 'hr@example.com', 'otp' => '123456'],
|
||||
$hrModel,
|
||||
$authHistoryModel
|
||||
);
|
||||
|
||||
$body = $this->decodeResponse($controller->getVerifiedHrData());
|
||||
|
||||
$this->assertSame('success', $body['status']);
|
||||
$this->assertSame(['post' => [1, 5]], $body['data'][0]['allowed_modules']);
|
||||
$this->assertSame([], $body['data'][0]['claims_sub_menu']);
|
||||
$this->assertNotEmpty($body['data'][0]['token']);
|
||||
}
|
||||
|
||||
public function testMissingHrAccessReturnsEmptyModulesAndEmptyToken()
|
||||
{
|
||||
$branchRow = [
|
||||
'id' => 5,
|
||||
'mobile' => '9876543210',
|
||||
'email' => 'hr@example.com',
|
||||
'client_id' => 42,
|
||||
'client_name' => 'Acme Corp',
|
||||
'short_name' => 'ACME',
|
||||
'client_branch_id' => 7,
|
||||
'branch_name' => 'HQ',
|
||||
'post_branch_id' => 70,
|
||||
];
|
||||
|
||||
$hrModel = $this->createHrModelStub([
|
||||
'first' => ['id' => 5, 'email' => 'hr@example.com', 'mobile' => '9876543210'],
|
||||
'findAll' => [$branchRow],
|
||||
'update' => true,
|
||||
]);
|
||||
|
||||
$authHistoryModel = $this->createMock(AuthHistoryModel::class);
|
||||
$authHistoryModel->method('insert')->willReturn(1);
|
||||
|
||||
$this->injectDbConnection('postDB', $this->createQueryResultStub(null));
|
||||
|
||||
$controller = $this->buildController(
|
||||
['mobile_no' => '9876543210', 'otp' => '123456'],
|
||||
$hrModel,
|
||||
$authHistoryModel
|
||||
);
|
||||
|
||||
$body = $this->decodeResponse($controller->getVerifiedHrData());
|
||||
|
||||
$this->assertSame('success', $body['status']);
|
||||
$this->assertSame(200, $body['code']);
|
||||
$this->assertSame([], $body['data'][0]['allowed_modules']);
|
||||
$this->assertSame('', $body['data'][0]['token']);
|
||||
}
|
||||
|
||||
private function buildController(
|
||||
array $payload,
|
||||
object $hrModel,
|
||||
?AuthHistoryModel $authHistoryModel = null
|
||||
): RestAuthenticationController {
|
||||
$json = (object) $payload;
|
||||
|
||||
$uri = $this->createMock(URI::class);
|
||||
$uri->method('getPath')->willReturn('/employeeRest/getVerifiedHrData');
|
||||
$uri->method('getSegments')->willReturn(['employeeRest', 'getVerifiedHrData']);
|
||||
|
||||
$request = $this->getMockBuilder(IncomingRequest::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods([
|
||||
'getJSON',
|
||||
'getHeaderLine',
|
||||
'getIPAddress',
|
||||
'getMethod',
|
||||
'getPost',
|
||||
'getVar',
|
||||
])
|
||||
->getMock();
|
||||
|
||||
$request->uri = $uri;
|
||||
$request->method('getJSON')->willReturn($json);
|
||||
$request->method('getHeaderLine')->with('User-Agent')->willReturn('PHPUnit');
|
||||
$request->method('getIPAddress')->willReturn('127.0.0.1');
|
||||
$request->method('getMethod')->willReturn('post');
|
||||
$request->method('getPost')->willReturn([]);
|
||||
$request->method('getVar')->willReturnCallback(static function ($key) {
|
||||
return match ($key) {
|
||||
'rateLimitFingerprint' => 'test-fingerprint',
|
||||
'rateLimitIdentity' => 'hr@example.com',
|
||||
default => null,
|
||||
};
|
||||
});
|
||||
|
||||
Services::injectMock('request', $request);
|
||||
|
||||
$controller = new RestAuthenticationController();
|
||||
$controller->initController($request, Services::response(), Services::logger());
|
||||
|
||||
$reflection = new ReflectionClass($controller);
|
||||
|
||||
$hrProp = $reflection->getProperty('hrModel');
|
||||
$hrProp->setAccessible(true);
|
||||
$hrProp->setValue($controller, $hrModel);
|
||||
|
||||
if ($authHistoryModel !== null) {
|
||||
$authProp = $reflection->getProperty('authHistoryModel');
|
||||
$authProp->setAccessible(true);
|
||||
$authProp->setValue($controller, $authHistoryModel);
|
||||
}
|
||||
|
||||
return $controller;
|
||||
}
|
||||
|
||||
private function createHrModelStub(array $results): object
|
||||
{
|
||||
return new class ($results) {
|
||||
private array $results;
|
||||
private $setPayload = null;
|
||||
|
||||
public function __construct(array $results)
|
||||
{
|
||||
$this->results = $results;
|
||||
}
|
||||
|
||||
public function select(...$args)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function join(...$args)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function where(...$args)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function set($data)
|
||||
{
|
||||
$this->setPayload = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function first()
|
||||
{
|
||||
return $this->results['first'] ?? null;
|
||||
}
|
||||
|
||||
public function findAll()
|
||||
{
|
||||
return $this->results['findAll'] ?? [];
|
||||
}
|
||||
|
||||
public function update(...$args)
|
||||
{
|
||||
return $this->results['update'] ?? true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fluent DB stub returning either a single row (getRowArray) or many rows (getResultArray).
|
||||
*/
|
||||
private function createQueryResultStub(?array $row, array $rows = []): object
|
||||
{
|
||||
return new class ($row, $rows) {
|
||||
private ?array $row;
|
||||
private array $rows;
|
||||
|
||||
public function __construct(?array $row, array $rows)
|
||||
{
|
||||
$this->row = $row;
|
||||
$this->rows = $rows;
|
||||
}
|
||||
|
||||
public function table(...$args)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function select(...$args)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function join(...$args)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function where(...$args)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function whereIn(...$args)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function get()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRowArray(): ?array
|
||||
{
|
||||
return $this->row;
|
||||
}
|
||||
|
||||
public function getResultArray(): array
|
||||
{
|
||||
return $this->rows;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private function injectDbConnection(string $group, object $connection): void
|
||||
{
|
||||
$property = new ReflectionProperty(DatabaseConfig::class, 'instances');
|
||||
$property->setAccessible(true);
|
||||
|
||||
$instances = $property->getValue() ?? [];
|
||||
$instances[$group] = $connection;
|
||||
$property->setValue(null, $instances);
|
||||
}
|
||||
|
||||
private function clearInjectedDbConnections(): void
|
||||
{
|
||||
$property = new ReflectionProperty(DatabaseConfig::class, 'instances');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue(null, []);
|
||||
}
|
||||
|
||||
private function mockCurlRequest(array $payload): void
|
||||
{
|
||||
$response = $this->createMock(ResponseInterface::class);
|
||||
$response->method('getBody')->willReturn(json_encode($payload));
|
||||
|
||||
$curl = $this->getMockBuilder(\CodeIgniter\HTTP\CURLRequest::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['post', 'get'])
|
||||
->getMock();
|
||||
$curl->method('post')->willReturn($response);
|
||||
$curl->method('get')->willReturn($response);
|
||||
|
||||
Services::injectMock('curlrequest', $curl);
|
||||
}
|
||||
|
||||
private function mockRateLimiter(): void
|
||||
{
|
||||
$limiter = $this->getMockBuilder(\App\Libraries\RateLimiterService::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['recordIpFailure', 'recordUserFailure'])
|
||||
->getMock();
|
||||
$limiter->method('recordIpFailure')->willReturn(null);
|
||||
$limiter->method('recordUserFailure')->willReturn(null);
|
||||
|
||||
Services::injectMock('limiter', $limiter);
|
||||
}
|
||||
|
||||
private function decodeResponse($response): array
|
||||
{
|
||||
return json_decode($response->getBody(), true);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user