Compare commits

...

No commits in common. "31359b11951a3824cf769471610e8056cfbaa032" and "cff90ed42efeeced9b90e49d066a7d10c910d8d9" have entirely different histories.

3271 changed files with 2094758 additions and 1 deletions

View File

@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(find /c/xampp/htdocs/PHP828APPS/ruc/nhance -type f -name *.php -path */migration*)",
"Bash(find /c/xampp/htdocs/PHP828APPS/ruc/nhance -type f \\\\\\(-name *.sql -o -name schema* \\\\\\))"
]
}
}

5
.cpanel.yml Executable file
View File

@ -0,0 +1,5 @@
---
deployment:
tasks:
- export DEPLOYPATH=home/venbaehn/public_html/nhance/devauto
- /bin/cp /home/venbaehn/repositories/nhance_dev/* $DEPLOYPATH -r

251
.env.sample Executable file
View File

@ -0,0 +1,251 @@
#--------------------------------------------------------------------
# ENVIRONMENT
#--------------------------------------------------------------------
CI_ENVIRONMENT =
#--------------------------------------------------------------------
# APP
#--------------------------------------------------------------------
app.baseURL =
app.appTimezone =
# If you have trouble with `.`, you could also use `_`.
# app_baseURL = ''
# app.forceGlobalSecureRequests = false
# app.CSPEnabled = false
#--------------------------------------------------------------------
# DATABASE
#--------------------------------------------------------------------
DB_SSL_ENABLE =
database.default.hostname =
database.default.database =
database.default.username =
database.default.password =
database.default.DBDriver =
#database.default.DBPrefix =
#database.default.port =
#--------------------------------------------------------------------
# SESSION
#--------------------------------------------------------------------
# session.driver = 'CodeIgniter\Session\Handlers\FileHandler'
# session.cookieName = 'ci_session'
session.expiration =
# session.savePath = null
# session.matchIP = false
session.timeToUpdate = 300
session.regenerateDestroy = false
#--------------------------------------------------------------------
# LOGGER
#--------------------------------------------------------------------
logger.threshold = 4
#--------------------------------------------------------------------
# CURLRequest
#--------------------------------------------------------------------
# curlrequest.shareOptions = true
GOOGLE_OAUTH_APP_NAME =
#GOOGLE_OAUTH_CLIENT_ID =
#GOOGLE_OAUTH_CLIENT_SECRET =
GOOGLE_OAUTH_CLIENT_ID =
GOOGLE_OAUTH_CLIENT_SECRET =
GOOGLE_OAUTH_REDIRECT_URI =
GOOGLE_OAUTH_SCOPES =
App_Url =
NHANCE_LOGO =
helpdeskURL =
TOKENTIMEOUT =
# Grace seconds after token_time_out epoch before reset-token-timeout cron clears it (default 180 = 3 min)
TOKEN_TIMEOUT_RESET_BUFFER_SECONDS = 180
POST_ENROLLMENT_APP_LINK =
GDRIVE_ROOT_FOLDER_ID =
RFQ_PARENT_FOLDER_ID =
email.fromEmail =
email.fromName =
ZEPTO_API_KEY =
email.SMTPPass =
#--------------------------------------------------------------------
# SET COOKIE DATA
#--------------------------------------------------------------------
cookie.Path = '/';
cookie.Domain = '.nhanceindia.in'; //Enter Domain Name with preceding .(dot) -> .nhanceindia.in
cookie.secure = 'true';// if https set as true or if http set as false
unlayer.projectID =
email.enquiryMail =
#SMS
SMS_API_KEY =
SMS_SENDER_ID =
SMS_TEMPLATE_ID =
SMS_SERVICE_NAME =
SMS_APP_NAME =
# CORS Configuration
CORS_ALLOWED_ORIGINS=
CORS_ALLOW_CREDENTIALS=true
CORS_ALLOWED_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
CORS_ALLOWED_HEADERS=Content-Type,Authorization,X-Requested-With,Accept,Origin,App-Signature
CORS_EXPOSE_HEADERS=X-Total-Count,X-Page-Count,X-RateLimit-Remaining
CORS_MAX_AGE=7200
CORS_DEBUG=true
APP_SIGNATURE =
TOKENTIMEOUT =
JWT_SECRET =
ICICI_PRIMARY_KEY_CONSTANT =
ABHI_PRIMARY_KEY_CONSTANT =
R_CARE_PRIMARY_KEY_CONSTANT =
FHPL_PRIMARY_KEY_CONSTANT =
VIDAL_PRIMARY_KEY_CONSTANT =
MEDI_ASSIST_PRIMARY_KEY_CONSTANT =
#--------------------------------------------------------------------
# TPA PRIMARY KEY CONSTANTS LIVE
#--------------------------------------------------------------------
MEDI_ASSIST_PRIMARY_KEY_CONSTANT_LIVE =
ICICI_PRIMARY_KEY_CONSTANT_LIVE =
ABHI_PRIMARY_KEY_CONSTANT_LIVE =
R_CARE_PRIMARY_KEY_CONSTANT_LIVE =
FHPL_PRIMARY_KEY_CONSTANT_LIVE =
VIDAL_PRIMARY_KEY_CONSTANT_LIVE =
VOLO_PRIMARY_KEY_CONSTANT_LIVE =
# When true, claims-collection-report falls back to ticket_master only if the
# policy TPA has no mapped dump table (or dump table missing). Dump TPAs always use claim_report.
CLAIM_REPORT_FALLBACK_TO_TICKET_MASTER = false
FHPL_TOKEN_URL =
FHPL_BASE_URL =
FHPL_USER_NAME =
FHPL_PASSWORD =
FHPL_GRANT_TYPE =
METABASE_SECRET_KEY=
# Health care api details
HEALTH_INDIA_BASE_URL =
HEALTH_INDIA_TOKEN_URL =
HEALTH_INDIA_USERNAME =
HEALTH_INDIA_PASSWORD =
HEALTH_INDIA_PRIMARY_KEY_CONSTANT =
# Volo / TrueCover (EWA) — ewatpa.com APIs
VOLO_PRIMARY_KEY_CONSTANT =
VOLO_API_ADMIN_BASE_URL = https://uatapiadmin.ewatpa.com
VOLO_API_CONSUMER_BASE_URL = https://uatapiconsumer.ewatpa.com
VOLO_API_EMAIL =
VOLO_API_PASSWORD =
VOLO_LOGGED_IN_PORTAL = POLICY_CONFIGURATION_PORTAL
# Optional: if get-entity-from-policy cannot be used
VOLO_DEFAULT_ENTITY_ID =
# Required for claim intimation — hospital id from Volo network master
VOLO_DEFAULT_HOSPITAL_ID =
#For sending mail for leads RFQ/QCR
LEAD_INSURER_FROM_MAIL_ID =
LEAD_CLIENT_FROM_MAIL_ID =
# BDS Daily Report Emails Configuration
bds.dailyReportEmails =
# BDS Installment Reminder (pending UTR / overdue) — used by getLeadInstallmentDetails / sendInstallmentRemainderMail cron
# Master switch: when false, no installment reminder records are fetched
bds.installmentReminder.enabled = true
# Business days ahead of today to match upcoming payment_date (was hardcoded 5)
bds.installmentReminder.businessDays = 5
# When true, also fetch records with null UTR whose payment_date is already past (overdue)
bds.installmentReminder.fetchOverduePendingUtr = false
# When true, fetch every day; when false, only on days listed in bds.installmentReminder.days
bds.installmentReminder.daily = true
# Comma-separated weekdays (mon,tue,wed,thu,fri,sat,sun) — used only when daily = false
bds.installmentReminder.days = mon,tue,wed,thu,fri
# Policy Transaction Renewal Reminder — used by sendRenewalReminderMail cron
pt.renewalReminder.enabled = true
pt.renewalReminder.daily = true
pt.renewalReminder.days = mon,tue,wed,thu,fri
# Client types: 1=Group, 2=Individual
pt.renewalReminder.clientTypes = 1,2
# renewal_date | policy_end_date
pt.renewalReminder.dateField = renewal_date
pt.renewalReminder.daysBefore = 30, 15, 7, 1, -2, -5, -10
pt.renewalReminder.includeClientEmail = true
# When true, also send for policies whose dateField is already past (overdue)
pt.renewalReminder.includeOverdue = false
pt.renewalReminder.toEmails =
pt.renewalReminder.ccEmails =
pt.renewalReminder.bccEmails =
pt.renewalReminder.fromMail =
#--------------------------------------------------------------------
# MEDI ASSIST WELLNESS SSO Configuration
#--------------------------------------------------------------------
MEDIASSIST_WELLNESS_KEY =
MEDIASSIST_WELLNESS_IV =
MEDIASSIST_WELLNESS_LOGIN_URL =
#--------------------------------------------------------------------
# Icici api details LIVE
#--------------------------------------------------------------------
ICICI_TOKEN_URL =
ICICI_BASE_URL =
ICICI_PASSWORD =
ICICI_CLIENT_ID =
ICICI_CLIENT_SECRET =
ICICI_API_SCOPE =
ICICI_GRANT_TYPE =
ICICI_PRIMARY_KEY_CONSTANT =
#--------------------------------------------------------------------
# Digit Motor (OneAPI)
#--------------------------------------------------------------------
DIGIT_MOTOR_BASE_URL = https://preprod-oneapi.godigit.com
DIGIT_MOTOR_AUTH_PATH = /OneAPI/v1/auth
DIGIT_MOTOR_EXECUTOR_PATH = /OneAPI/v1/executor
DIGIT_MOTOR_USERNAME =
DIGIT_MOTOR_PASSWORD =
DIGIT_MOTOR_ENVIRONMENT = staging
DIGIT_MOTOR_TIMEOUT = 30
DIGIT_MOTOR_TOKEN_LEEWAY_SEC = 60
DIGIT_MOTOR_PDF_AUTH_KEY =
DIGIT_MOTOR_MASTERS_PATH =
DIGIT_MOTOR_IID_QUICK_QUOTE = 29266-0100
DIGIT_MOTOR_IID_CREATE_QUOTE = 29268-0100
DIGIT_MOTOR_IID_KYC = 29269-0100
DIGIT_MOTOR_IID_PAYMENT = 29270-0100
DIGIT_MOTOR_IID_POLICY_STATUS = 29271-0100
DIGIT_MOTOR_IID_POLICY_PDF = 29272-0100
#--------------------------------------------------------------------
# File storage (S3 uploads)
#--------------------------------------------------------------------
# FILE_STORAGE_DRIVER = s3
# AWS_FILE_UPLOAD_BUCKET =
# Keep permanent local copy after S3 upload in storage_file_Upload (default false)
RETAIN_LOCAL = false

45
.gitignore vendored Executable file
View File

@ -0,0 +1,45 @@
#-------------------------
# Temporary Files
#-------------------------
writable/cache/*
!writable/cache/.gitkeep
!writable/cache/storage_runtime/
!writable/cache/storage_runtime/.gitkeep
!writable/cache/claim_files_runtime/
!writable/cache/claim_files_runtime/.gitkeep
writable/logs/*
!writable/logs/.gitkeep
writable/session/*
!writable/session/.gitkeep
writable/uploads/*
!writable/uploads/.gitkeep
public/uploads/*
!public/uploads/.gitkeep
writable/debugbar/*
!writable/debugbar/.gitkeep
writable/**/*.db
writable/**/*.sqlite
# writable/e_card_template/*
# !writable/e_card_template/.gitkeep
writable/tmp/*
!writable/tmp/.gitkeep
writable/insurer_claim_form/*
!writable/insurer_claim_form/Claim_Form_IRDAI.pdf
vendor/
build/
composer.lock
.env
.phpunit*
phpqueue.sh
.claude

67
.htaccess Executable file
View File

@ -0,0 +1,67 @@
# Disable directory browsing
Options -Indexes
# ----------------------------------------------------------------------
# Rewrite engine
# ----------------------------------------------------------------------
# Turning on the rewrite engine is necessary for the following rules and features.
# FollowSymLinks must be enabled for this to work.
<IfModule mod_rewrite.c>
Options +FollowSymlinks
RewriteEngine On
# If you installed CodeIgniter in a subfolder, you will need to
# change the following line to match the subfolder you need.
# http://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritebase
# RewriteBase /
# Redirect Trailing Slashes...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Rewrite "www.example.com -> example.com"
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L]
# Checks to see if the user is attempting to access a valid file,
# such as an image or css document, if this isn't true it sends the
# request to the front controller, index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([\s\S]*)$ public/$1 [L,NC,QSA]
# Ensure Authorization header is passed along
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
</IfModule>
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access 1 year"
ExpiresByType image/jpeg "access 1 year"
ExpiresByType image/gif "access 1 year"
ExpiresByType image/png "access 1 year"
ExpiresByType text/css "access 1 month"
ExpiresByType text/js "access 1 month"
ExpiresByType application/pdf "access 1 month"
ExpiresByType application/javascript "access 1 month"
ExpiresByType application/x-javascript "access 1 month"
ExpiresByType application/x-shockwave-flash "access 1 month"
ExpiresByType image/x-icon "access 1 year"
ExpiresDefault "access 2 days"
</IfModule>
<IfModule !mod_rewrite.c>
# If we don't have mod_rewrite installed, all 404's
# can be sent to index.php, and everything works as normal.
ErrorDocument 404 index.php
</IfModule>
# Disable server signature start
ServerSignature Off
# Disable server signature end

Binary file not shown.

22
LICENSE Executable file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014-2019 British Columbia Institute of Technology
Copyright (c) 2019-2023 CodeIgniter Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

37
README.md Normal file → Executable file
View File

@ -1,2 +1,37 @@
# Nhance
# CodeIgniter 4 Framework
## Unit Testing
From command propmt run the following cmds
`php vendor/bin/phpunit tests\unit\GridType11PremiumCalculationTest.php`
Run speeific method
`php vendor/bin/phpunit tests\unit\PremiumCalculationTest.php --filter testPremiumCalculationWithPrimaryRackRateAndAdditionalRackRate`
Test Comments
## Non-EB Claims API Tests
Run all 4 Non-EB API tests at once:
`php vendor/bin/phpunit tests\unit\Api`
Run a single test file:
`php vendor/bin/phpunit tests\unit\Api\CreateClaimTest.php`
`php vendor/bin/phpunit tests\unit\Api\ListClaimsTest.php`
`php vendor/bin/phpunit tests\unit\Api\ClaimHistoryTest.php`
`php vendor/bin/phpunit tests\unit\Api\UploadRequiredDocTest.php`
Run a specific test method:
`php vendor/bin/phpunit tests\unit\Api\CreateClaimTest.php --filter testReturns401WhenNoAuth`
Run with verbose output (shows each test name):
`php vendor/bin/phpunit tests\unit\Api --testdox`

6
app/.htaccess Executable file
View File

@ -0,0 +1,6 @@
<IfModule authz_core_module>
Require all denied
</IfModule>
<IfModule !authz_core_module>
Deny from all
</IfModule>

View File

@ -0,0 +1,103 @@
<?php
namespace App\Commands;
use App\Libraries\DigitMotor\DigitMasterImportService;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Create Digit motor master tables and import kit Excel files.
*
* Usage:
* php spark digit-motor:import-masters --create-tables
* php spark digit-motor:import-masters --path="/path/to/Masters"
* php spark digit-motor:import-masters --only=products,previous_insurers,ncb
* php spark digit-motor:import-masters --skip-vehicles
*/
class DigitMotorImportMasters extends BaseCommand
{
protected $group = 'DigitMotor';
protected $name = 'digit-motor:import-masters';
protected $description = 'Create motor_master_* tables and import Digit Masters Excel files.';
protected $usage = 'digit-motor:import-masters [--path] [--create-tables] [--only] [--skip-vehicles]';
protected $options = [
'--path' => 'Path to Digit Masters folder (xlsx files).',
'--create-tables' => 'Run digit_motor_master_tables.sql first.',
'--only' => 'Comma-separated master keys to import.',
'--skip-vehicles' => 'Skip large vehicle master import.',
];
public function run(array $params)
{
$defaultPath = '/home/smart/Downloads/Digit API Integration_API kit (1)/Masters';
$path = CLI::getOption('path') ?: env('DIGIT_MOTOR_MASTERS_PATH', $defaultPath);
$path = rtrim((string) $path, '/');
if (!is_dir($path)) {
CLI::error('Masters path not found: ' . $path);
return EXIT_ERROR;
}
$service = new DigitMasterImportService($path);
$service->setProgressCallback(static function (string $msg) {
CLI::write($msg, 'yellow');
});
if (CLI::getOption('create-tables') !== null) {
$sql = ROOTPATH . 'app/Database/digit_motor_master_tables.sql';
CLI::write('Creating tables from ' . $sql, 'green');
try {
$service->createTables($sql);
CLI::write('Tables created / verified.', 'green');
} catch (\Throwable $e) {
CLI::error('Create tables failed: ' . $e->getMessage());
return EXIT_ERROR;
}
}
$onlyOpt = CLI::getOption('only');
// CI may return true for `--only=vehicles` when value parsing fails — also check argv
if ($onlyOpt === true || $onlyOpt === null || $onlyOpt === '') {
foreach ($_SERVER['argv'] ?? [] as $arg) {
if (str_starts_with((string) $arg, '--only=')) {
$onlyOpt = substr((string) $arg, 7);
break;
}
}
}
$only = [];
if (is_string($onlyOpt) && $onlyOpt !== '' && $onlyOpt !== '1') {
$only = array_filter(array_map('trim', explode(',', $onlyOpt)));
}
if (CLI::getOption('skip-vehicles') !== null) {
if (!$only) {
$only = [
'products', 'previous_insurers', 'ncb', 'voluntary_deductible',
'previous_policy_type', 'doc_types', 'nominee_relations', 'states',
'sub_products', 'addon_age_limits', 'pincodes', 'rtos',
];
} else {
$only = array_values(array_diff($only, ['vehicles']));
}
}
CLI::write('Importing from: ' . $path, 'green');
$results = $service->importAll($only);
CLI::newLine();
CLI::write(str_pad('Master', 28) . str_pad('Rows', 10) . 'Status', 'white');
CLI::write(str_repeat('-', 55));
foreach ($results as $key => $res) {
$line = str_pad($key, 28) . str_pad((string) $res['rows'], 10) . $res['status'];
if (!empty($res['message'])) {
$line .= ' — ' . $res['message'];
}
CLI::write($line, $res['status'] === 'OK' ? 'green' : 'red');
}
$failed = array_filter($results, static fn ($r) => $r['status'] !== 'OK');
return $failed ? EXIT_ERROR : EXIT_SUCCESS;
}
}

View File

@ -0,0 +1,217 @@
<?php
namespace App\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
class MigrateLocalFilesToS3 extends BaseCommand
{
protected $group = 'Storage';
protected $name = 'storage:migrate-local-to-s3';
protected $description = 'One-time phased migration of local uploaded files to S3 using FileStorageService.';
protected $usage = 'storage:migrate-local-to-s3 [--module writable_uploads|<folder_name>|insurer_claim_form|all] [--limit 500] [--offset 0] [--execute]';
protected $options = [
'--module' => 'Target module/folder (default: writable_uploads). Use writable_uploads for all writable/uploads/* folders.',
'--limit' => 'Max files per run (default: 500).',
'--offset' => 'Start offset for phased batches (default: 0).',
'--execute' => 'Actually upload files. Without this flag, command runs in dry-run mode.',
];
/**
* @var array<string, string>
*/
private array $staticPaths = [
'insurer_claim_form' => WRITEPATH . 'insurer_claim_form',
];
public function run(array $params)
{
$moduleOpt = strtolower((string) (CLI::getOption('module') ?? 'writable_uploads'));
$execute = CLI::getOption('execute') !== null;
$dryRun = !$execute;
$limit = max(1, (int) (CLI::getOption('limit') ?? 500));
$offset = max(0, (int) (CLI::getOption('offset') ?? 0));
$modulePaths = $this->buildModulePaths();
$selectedModules = $this->resolveModules($moduleOpt, $modulePaths);
if ($selectedModules === []) {
CLI::error('Invalid --module value. Allowed: writable_uploads, ' . implode(', ', array_keys($modulePaths)) . ', all');
return;
}
$storage = \Config\Services::getFileStorageService(false);
if (!$storage->usesS3()) {
CLI::error('FileStorageService is not in S3 mode. Set FILE_STORAGE_DRIVER=s3 before migration.');
return;
}
CLI::write($dryRun ? 'Running in DRY-RUN mode (no uploads).' : 'Running in EXECUTE mode (uploads enabled).', 'yellow');
CLI::write('Target bucket: ' . ($storage->getBucket() ?? 'N/A'), 'cyan');
CLI::newLine();
$totals = [
'seen' => 0,
'uploaded' => 0,
'skipped_exists' => 0,
'skipped_missing' => 0,
'failed' => 0,
];
foreach ($selectedModules as $module) {
$path = $modulePaths[$module];
CLI::write("Module: {$module}", 'green');
CLI::write("Path: {$path}");
if (!is_dir($path)) {
CLI::write(' - Skipped: folder does not exist', 'yellow');
$totals['skipped_missing']++;
CLI::newLine();
continue;
}
$files = $this->listFiles($path);
$files = array_slice($files, $offset, $limit);
CLI::write(' - Files in batch: ' . count($files));
foreach ($files as $fullPath) {
$totals['seen']++;
$fileName = basename($fullPath);
if (!is_file($fullPath)) {
$totals['skipped_missing']++;
CLI::write(" - Missing: {$fileName}", 'yellow');
continue;
}
$alreadyExists = $storage->exists($path, $fileName);
if ($alreadyExists) {
$totals['skipped_exists']++;
CLI::write(" - Exists in S3, skipped: {$fileName}", 'yellow');
continue;
}
if ($dryRun) {
CLI::write(" - [dry-run] Would upload: {$fileName}");
continue;
}
$result = $storage->upload($fullPath, $path, $fileName);
if ($result['success'] ?? false) {
$totals['uploaded']++;
CLI::write(" - Uploaded: {$fileName}", 'green');
} else {
$totals['failed']++;
CLI::write(" - Failed: {$fileName} | " . ($result['message'] ?? 'unknown error'), 'red');
}
}
CLI::newLine();
}
CLI::write('Migration summary', 'cyan');
CLI::write(' Seen: ' . $totals['seen']);
CLI::write(' Uploaded: ' . $totals['uploaded'], $totals['uploaded'] > 0 ? 'green' : 'white');
CLI::write(' Skipped (already in S3): ' . $totals['skipped_exists'], 'yellow');
CLI::write(' Skipped (missing/invalid): ' . $totals['skipped_missing'], 'yellow');
CLI::write(' Failed: ' . $totals['failed'], $totals['failed'] > 0 ? 'red' : 'white');
if ($dryRun) {
CLI::newLine();
CLI::write('Dry-run complete. Re-run with --execute to perform uploads.', 'yellow');
}
}
/**
* @return array<int, string>
*/
private function resolveModules(string $moduleOpt, array $modulePaths): array
{
if ($moduleOpt === 'all') {
return array_keys($modulePaths);
}
if ($moduleOpt === 'writable_uploads') {
$uploadRoot = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
$modules = [];
foreach ($modulePaths as $module => $path) {
$normalized = str_replace('\\', '/', $path);
$normalizedRoot = str_replace('\\', '/', $uploadRoot);
if (strpos($normalized, rtrim($normalizedRoot, '/')) === 0) {
$modules[] = $module;
}
}
return $modules;
}
$raw = array_filter(array_map('trim', explode(',', $moduleOpt)));
$valid = [];
foreach ($raw as $module) {
if (array_key_exists($module, $modulePaths)) {
$valid[] = $module;
}
}
return array_values(array_unique($valid));
}
/**
* Build migratable paths:
* - all direct subfolders in writable/uploads
* - additional static paths (outside writable/uploads)
*
* @return array<string, string>
*/
private function buildModulePaths(): array
{
$paths = [];
$uploadRoot = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads';
$entries = @scandir($uploadRoot);
if (is_array($entries)) {
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$fullPath = $uploadRoot . DIRECTORY_SEPARATOR . $entry;
if (is_dir($fullPath)) {
$paths[strtolower($entry)] = $fullPath;
}
}
}
foreach ($this->staticPaths as $key => $path) {
$paths[$key] = $path;
}
ksort($paths);
return $paths;
}
/**
* @return array<int, string>
*/
private function listFiles(string $path): array
{
$entries = @scandir($path);
if (!is_array($entries)) {
return [];
}
$files = [];
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$fullPath = rtrim($path, '/\\') . DIRECTORY_SEPARATOR . $entry;
if (is_file($fullPath)) {
$files[] = $fullPath;
}
}
sort($files);
return $files;
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Commands;
use App\Libraries\RateLimiterService;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use Config\Database;
use Config\RateLimiter as RateLimiterConfig;
/**
* Align rate_limit_blocks with timed cache TTL: purge stale cache keys and delete DB rows.
*
* Schedule (example every 10 minutes):
* *\/10 * * * * cd /path/to/project && php spark rate-limit:reconcile-blocks
*/
class RateLimitBlocksReconcile extends BaseCommand
{
protected $group = 'Rate limit';
protected $name = 'rate-limit:reconcile-blocks';
protected $description = 'For active rows whose block TTL has passed: purge cache keys, then delete the DB row.';
protected $usage = 'rate-limit:reconcile-blocks [--dry-run]';
/** @var array<string, string> */
protected $options = [
'--dry-run' => 'Show which rows would be purged without deleting cache or DB.',
];
public function run(array $params)
{
$dryRun = CLI::getOption('dry-run') !== null;
$db = Database::connect();
if (! $db->tableExists('rate_limit_blocks')) {
CLI::write('Table rate_limit_blocks does not exist. Nothing to do.', 'yellow');
return;
}
/** @var RateLimiterConfig $rl */
$rl = config('RateLimiter');
$limiter = new RateLimiterService();
$rows = $db->table('rate_limit_blocks')
->where('status', 'active')
->get()
->getResultArray();
$count = 0;
foreach ($rows as $row) {
$duration = $this->blockSecondsForRow($row, $rl);
if ($duration <= 0) {
continue;
}
$blockedAt = strtotime((string) $row['blocked_at']);
if ($blockedAt === false) {
CLI::write('Skipping id ' . $row['id'] . ': invalid blocked_at.', 'red');
continue;
}
if (time() < $blockedAt + $duration) {
continue;
}
$id = (int) $row['id'];
$cacheId = (string) $row['cache_identifier'];
$blockType = (string) $row['block_type'];
CLI::write(
($dryRun ? '[dry-run] Would reconcile ' : 'Reconciling ')
. "{$blockType} id={$id} level={$row['block_level']} display=" . $row['display_identifier'],
'cyan'
);
if (! $dryRun) {
if ($blockType === 'ip') {
$limiter->purgeIpBlockCaches($cacheId);
} elseif ($blockType === 'user') {
$limiter->purgeUserBlockCaches($cacheId);
}
$db->table('rate_limit_blocks')->delete(['id' => $id], 1);
}
$count++;
}
CLI::write(
$dryRun
? "Dry run complete. {$count} row(s) would be purged and deleted."
: "Done. Reconciled {$count} expired row(s).",
'yellow'
);
}
/**
* @param array<string, mixed> $row
*/
protected function blockSecondsForRow(array $row, RateLimiterConfig $cfg): int
{
$blockCfg = ($row['block_type'] ?? '') === 'ip' ? $cfg->ipBlock : $cfg->userBlock;
$level = (string) ($row['block_level'] ?? '');
return match ($level) {
'soft' => (int) $blockCfg['soft_duration'],
'medium' => (int) $blockCfg['medium_duration'],
'hard' => (int) $blockCfg['hard_duration'],
default => 0,
};
}
}

View File

@ -0,0 +1,319 @@
<?php
namespace App\Commands;
use App\Libraries\TpaClaimsImportFactory;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Find TPA dump rows whose ticket_id is missing/inactive in ticket_master,
* clear the orphan link, then run Job 2 to insert tickets again.
*
* Usage:
* php spark tpa:recreate-orphan-tickets
* php spark tpa:recreate-orphan-tickets --tpa=icici
* php spark tpa:recreate-orphan-tickets --tpa-id=6 --apply
* php spark tpa:recreate-orphan-tickets --file-id=84 --apply
* php spark tpa:recreate-orphan-tickets --tpa=all --apply
*/
class RecreateOrphanDumpTickets extends BaseCommand
{
protected $group = 'TPA';
protected $name = 'tpa:recreate-orphan-tickets';
protected $description = 'Recreate ticket_master rows for dump ticket_id orphans';
protected $usage = 'tpa:recreate-orphan-tickets [--tpa=NAME|all] [--tpa-id=ID] [--file-id=ID] [--include-pending] [--apply]';
protected $options = [
'--tpa' => 'TPA key: icici|abhi|mediassist|fhpl|rcare|vidal|all (default: all)',
'--tpa-id' => 'Numeric TPA primary key (overrides --tpa)',
'--file-id' => 'Limit to one claim_dump_files.id',
'--include-pending' => 'Also process dump rows with ticket_id IS NULL (never linked)',
'--apply' => 'Clear orphan links and run Job 2 (default is dry-run list only)',
];
private function tpaConfigs(): array
{
return [
'icici' => ['env' => 'ICICI_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_icici'],
'abhi' => ['env' => 'ABHI_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_abhi'],
'mediassist' => ['env' => 'MEDI_ASSIST_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_medi_assist'],
'fhpl' => ['env' => 'FHPL_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_fhpl'],
'rcare' => ['env' => 'R_CARE_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_reliance'],
'vidal' => ['env' => 'VIDAL_PRIMARY_KEY_CONSTANT', 'table' => 'claims_dump_vidal'],
];
}
public function run(array $params)
{
helper('utility_helper');
$db = db_connect();
$apply = $this->hasFlag('apply');
$includePending = $this->hasFlag('include-pending');
$fileIdFilter = (int) $this->resolveOptionValue('file-id', '0');
$configs = $this->tpaConfigs();
$selected = $this->resolveSelectedTpas($configs);
if ($selected === []) {
return;
}
$summary = [];
foreach ($selected as $tpaKey) {
$cfg = $configs[$tpaKey];
$tpaId = (int) env($cfg['env']);
$table = $cfg['table'];
CLI::newLine();
CLI::write(str_repeat('=', 64), 'yellow');
CLI::write(strtoupper($tpaKey) . " (tpa_id={$tpaId}, table={$table})", 'yellow');
CLI::write(str_repeat('=', 64), 'yellow');
if (!$this->tableExists($db, $table)) {
CLI::error("Table {$table} missing — skipped");
$summary[$tpaKey] = ['ok' => false, 'error' => 'table missing'];
continue;
}
$orphans = $this->findOrphans($db, $table, $fileIdFilter);
CLI::write('Orphan dump rows (ticket_id missing in ticket_master): ' . count($orphans));
$pending = [];
if ($includePending) {
$pending = $this->findPending($db, $table, $fileIdFilter);
CLI::write('Pending dump rows (ticket_id IS NULL): ' . count($pending));
}
if ($orphans === [] && $pending === []) {
$summary[$tpaKey] = ['ok' => true, 'orphans' => 0, 'pending' => 0, 'files' => 0];
continue;
}
foreach (array_slice($orphans, 0, 20) as $row) {
CLI::write(sprintf(
' [orphan] dump_id=%s file_id=%s ticket_id=%s',
$row['id'],
$row['file_id'],
$row['ticket_id']
));
}
if (count($orphans) > 20) {
CLI::write(' ... and ' . (count($orphans) - 20) . ' more orphans');
}
foreach (array_slice($pending, 0, 10) as $row) {
CLI::write(sprintf(
' [pending] dump_id=%s file_id=%s',
$row['id'],
$row['file_id']
));
}
$fileIds = array_values(array_unique(array_map(
'intval',
array_merge(array_column($orphans, 'file_id'), array_column($pending, 'file_id'))
)));
CLI::write('Affected file_ids: ' . implode(', ', $fileIds), 'cyan');
if (!$apply) {
CLI::write("Dry-run only. Apply with: php spark tpa:recreate-orphan-tickets --tpa={$tpaKey} --apply", 'yellow');
$summary[$tpaKey] = [
'ok' => true,
'orphans' => count($orphans),
'pending' => count($pending),
'files' => count($fileIds),
'dry_run' => true,
];
continue;
}
$fileResults = [];
foreach ($fileIds as $fileId) {
CLI::write("Processing file_id={$fileId} ...", 'light_red');
try {
$service = TpaClaimsImportFactory::make($tpaId);
$result = $service->recreateOrphanDumpTickets($fileId);
CLI::write(' result: ' . json_encode($result));
$stillOrphan = $this->countOrphansForFile($db, $table, $fileId);
$linked = $db->table($table)
->where('file_id', $fileId)
->where('is_active', 1)
->where('ticket_id IS NOT NULL', null, false)
->countAllResults();
$ok = !empty($result['status']) && $stillOrphan === 0;
$fileResults[] = [
'file_id' => $fileId,
'ok' => $ok,
'still_orphan' => $stillOrphan,
'linked' => $linked,
'message' => $result['message'] ?? null,
];
if (!$ok) {
CLI::error(" file_id={$fileId} still has {$stillOrphan} orphan(s) or Job2 failed");
} else {
CLI::write(" file_id={$fileId} OK (linked={$linked})", 'green');
}
if (!empty($result['status'])) {
$db->table('claim_dump_files')->where('id', $fileId)->update([
'status' => 'success',
'reason' => null,
]);
}
} catch (\Throwable $e) {
CLI::error(" file_id={$fileId} ERROR: " . $e->getMessage());
$fileResults[] = [
'file_id' => $fileId,
'ok' => false,
'error' => $e->getMessage(),
];
}
}
$allOk = !in_array(false, array_column($fileResults, 'ok'), true);
$summary[$tpaKey] = [
'ok' => $allOk,
'orphans' => count($orphans),
'pending' => count($pending),
'files' => count($fileIds),
'results' => $fileResults,
];
}
CLI::newLine();
CLI::write(str_repeat('=', 64), 'cyan');
CLI::write('SUMMARY', 'cyan');
CLI::write(str_repeat('=', 64), 'cyan');
foreach ($summary as $tpaKey => $row) {
$line = sprintf(
'%-12s %s orphans=%s pending=%s files=%s%s',
strtoupper($tpaKey),
!empty($row['ok']) ? 'OK' : 'FAIL',
$row['orphans'] ?? 0,
$row['pending'] ?? 0,
$row['files'] ?? 0,
!empty($row['dry_run']) ? ' (dry-run)' : (!empty($row['error']) ? ' | ' . $row['error'] : '')
);
CLI::write($line, !empty($row['ok']) ? 'green' : 'red');
}
if (!$apply) {
CLI::newLine();
CLI::write('No DB changes made. Re-run with --apply to clear orphans and insert tickets.', 'yellow');
}
}
/**
* Dump rows with ticket_id set but no active ticket_master row.
*/
private function findOrphans($db, string $table, int $fileIdFilter = 0): array
{
$sql = "SELECT d.id, d.file_id, d.ticket_id
FROM `{$table}` d
LEFT JOIN ticket_master tm ON tm.id = d.ticket_id AND tm.is_active = 1
WHERE d.is_active = 1
AND d.ticket_id IS NOT NULL
AND tm.id IS NULL";
$binds = [];
if ($fileIdFilter > 0) {
$sql .= ' AND d.file_id = ?';
$binds[] = $fileIdFilter;
}
$sql .= ' ORDER BY d.file_id ASC, d.id ASC';
return $db->query($sql, $binds)->getResultArray();
}
/**
* Dump rows never linked / rejected (eligible for Job 2 insert).
*/
private function findPending($db, string $table, int $fileIdFilter = 0): array
{
$builder = $db->table($table)
->select('id, file_id, ticket_id')
->where('is_active', 1)
->where('ticket_id IS NULL', null, false)
->where('master_reject_reason IS NULL', null, false);
if ($fileIdFilter > 0) {
$builder->where('file_id', $fileIdFilter);
}
return $builder->orderBy('file_id', 'ASC')->orderBy('id', 'ASC')->get()->getResultArray();
}
private function countOrphansForFile($db, string $table, int $fileId): int
{
$sql = "SELECT COUNT(*) AS cnt
FROM `{$table}` d
LEFT JOIN ticket_master tm ON tm.id = d.ticket_id AND tm.is_active = 1
WHERE d.is_active = 1
AND d.file_id = ?
AND d.ticket_id IS NOT NULL
AND tm.id IS NULL";
$row = $db->query($sql, [$fileId])->getRowArray();
return (int) ($row['cnt'] ?? 0);
}
private function resolveSelectedTpas(array $configs): array
{
$tpaIdOpt = (int) $this->resolveOptionValue('tpa-id', '0');
if ($tpaIdOpt > 0) {
foreach ($configs as $key => $cfg) {
if ((int) env($cfg['env']) === $tpaIdOpt) {
return [$key];
}
}
CLI::error("No TPA config matches --tpa-id={$tpaIdOpt}");
return [];
}
$tpaOpt = strtolower((string) ($this->resolveOptionValue('tpa', '') ?: 'all'));
if ($tpaOpt === '' || $tpaOpt === 'all') {
return array_keys($configs);
}
if (!isset($configs[$tpaOpt])) {
CLI::error("Unknown --tpa={$tpaOpt}. Use: " . implode('|', array_keys($configs)) . '|all');
return [];
}
return [$tpaOpt];
}
private function tableExists($db, string $table): bool
{
return !empty($db->query('SHOW TABLES LIKE ' . $db->escape($table))->getResultArray());
}
private function hasFlag(string $name): bool
{
if (CLI::getOption($name) !== null) {
return true;
}
return in_array('--' . $name, $_SERVER['argv'] ?? [], true);
}
private function resolveOptionValue(string $name, string $default): string
{
$opt = CLI::getOption($name);
if (is_string($opt) && $opt !== '') {
return $opt;
}
$argv = $_SERVER['argv'] ?? [];
foreach ($argv as $i => $arg) {
if (preg_match('/^--' . preg_quote($name, '/') . '=(.+)$/', (string) $arg, $m)) {
return trim($m[1]);
}
if ($arg === '--' . $name && isset($argv[$i + 1]) && strpos((string) $argv[$i + 1], '--') !== 0) {
return (string) $argv[$i + 1];
}
}
return $default;
}
}

View File

@ -0,0 +1,361 @@
<?php
namespace App\Commands;
use App\Models\ClaimFilesModel;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use Mpdf\Mpdf;
use Mpdf\Output\Destination;
/**
* Dedicated smoke test for claim PDF merge with S3-backed claim_files.
*
* Usage:
* php spark storage:smoke-test-merge-pdf
* php spark storage:smoke-test-merge-pdf --list
* php spark storage:smoke-test-merge-pdf --keep # leave S3/DB artifacts (debug)
*/
class StorageMergePdfSmokeTest extends BaseCommand
{
protected $group = 'Storage';
protected $name = 'storage:smoke-test-merge-pdf';
protected $description = 'Run dedicated merge_ticket_pdfs S3 scenarios (claim_files).';
protected $usage = 'storage:smoke-test-merge-pdf [--list] [--keep]';
protected $options = [
'--list' => 'List merge scenarios without executing.',
'--keep' => 'Skip cleanup of S3 objects and claim_files rows.',
];
private int $pass = 0;
private int $fail = 0;
private int $skip = 0;
/** @var list<array{id:string,title:string}> */
private array $catalog = [
['id' => 'MERGE-01', 'title' => 'Create & upload 2 source PDFs to S3 (claim_files, no permanent local)'],
['id' => 'MERGE-02', 'title' => 'Insert claim_files DB rows for synthetic ticket'],
['id' => 'MERGE-03', 'title' => 'merge_ticket_pdfs resolves sources from S3 (temp download)'],
['id' => 'MERGE-04', 'title' => 'Merge succeeds with pages >= 2'],
['id' => 'MERGE-05', 'title' => 'Merged PDF exists on S3'],
['id' => 'MERGE-06', 'title' => 'Merged permanent local path gone when S3 enabled'],
['id' => 'MERGE-07', 'title' => 'Merged claim_files row registered (file_type=4)'],
['id' => 'MERGE-08', 'title' => 'Claim runtime temps cleaned after merge'],
['id' => 'MERGE-09', 'title' => 'Cleanup removes S3 objects + soft-deletes DB rows'],
];
public function run(array $params)
{
helper(['utility', 'merge_pdf']);
if (CLI::getOption('list') !== null) {
CLI::write('========== MERGE PDF SCENARIOS ==========', 'cyan');
foreach ($this->catalog as $row) {
CLI::write(" {$row['id']} {$row['title']}");
}
CLI::newLine();
CLI::write('Run: php spark storage:smoke-test-merge-pdf');
return EXIT_SUCCESS;
}
$storage = \Config\Services::getFileStorageService(false);
$keep = CLI::getOption('keep') !== null;
CLI::write('========== CLAIM PDF MERGE SMOKE ==========', 'cyan');
CLI::write('usesS3: ' . ($storage->usesS3() ? 'YES' : 'NO'));
CLI::write('Bucket: ' . ($storage->getBucket() ?? 'N/A'));
CLI::write('RETAIN_LOCAL: ' . (storage_env_retain_local() ? 'true' : 'false'));
CLI::newLine();
if (! $storage->usesS3()) {
foreach ($this->catalog as $row) {
$this->skip($row['id'], 'FILE_STORAGE_DRIVER must be s3');
}
$this->printResult();
return EXIT_ERROR;
}
$uploadDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR
. 'uploads' . DIRECTORY_SEPARATOR
. 'claim_files' . DIRECTORY_SEPARATOR;
if (! is_dir($uploadDir)) {
@mkdir($uploadDir, 0775, true);
}
$ticketId = 990000000 + random_int(1000, 999999);
$stamp = date('Ymd_His') . '_' . bin2hex(random_bytes(3));
$srcNames = [
'smoke_merge_a_' . $stamp . '.pdf',
'smoke_merge_b_' . $stamp . '.pdf',
];
$sourceIds = [];
$mergedName = null;
$mergedId = null;
$claimFiles = new ClaimFilesModel();
try {
// MERGE-01
$uploaded = 0;
foreach ($srcNames as $i => $name) {
$local = $this->makeOnePagePdf($uploadDir, $name, 'Smoke source ' . ($i + 1));
if ($local === null || ! is_file($local)) {
continue;
}
$up = $storage->upload($local, rtrim($uploadDir, '/\\'), $name);
@unlink($local);
if (($up['success'] ?? false) && $storage->exists(rtrim($uploadDir, '/\\'), $name) && ! is_file($uploadDir . $name)) {
$uploaded++;
}
}
$this->step('MERGE-01', $uploaded === 2, "uploaded={$uploaded}/2 ticket_id={$ticketId}");
if ($uploaded !== 2) {
foreach (array_slice($this->catalog, 1) as $row) {
$this->skip($row['id'], 'source upload failed');
}
$this->printResult();
return EXIT_ERROR;
}
// MERGE-02
foreach ($srcNames as $idx => $name) {
$id = $claimFiles->insert([
'ticket_id' => $ticketId,
'file_type' => 2,
'doc_name' => 'SMOKE_MERGE_SRC_' . ($idx + 1),
'file_name' => $name,
'url' => $name,
'mime_type' => 'application/pdf',
'is_active' => 1,
'created_by' => 1,
]);
if ($id) {
$sourceIds[] = (int) $id;
}
}
$this->step('MERGE-02', count($sourceIds) === 2, 'ids=' . implode(',', $sourceIds));
if (count($sourceIds) !== 2) {
foreach (array_slice($this->catalog, 2) as $row) {
$this->skip($row['id'], 'DB insert failed');
}
$this->cleanup($storage, $uploadDir, $srcNames, null, $ticketId, $sourceIds, $keep);
$this->printResult();
return EXIT_ERROR;
}
// MERGE-03..07 via merge_ticket_pdfs
$result = merge_ticket_pdfs($ticketId, [
'replace' => true,
'created_by' => 1,
'include_file_types' => [1, 2],
'include_mime_types' => ['application/pdf', 'image/jpeg', 'image/png'],
]);
$this->step(
'MERGE-03',
(bool) ($result['status'] ?? false) && (int) ($result['source_count'] ?? 0) >= 2,
(string) ($result['message'] ?? '') . ' sources=' . (int) ($result['source_count'] ?? 0)
);
$pages = (int) ($result['pages'] ?? 0);
$this->step(
'MERGE-04',
(bool) ($result['status'] ?? false) && $pages >= 2,
'pages=' . $pages
);
$mergedName = (string) ($result['file_name'] ?? '');
$mergedId = $result['merged_file_id'] ?? null;
$onS3 = $mergedName !== '' && $storage->exists(rtrim($uploadDir, '/\\'), $mergedName);
$this->step('MERGE-05', $onS3, $mergedName !== '' ? $mergedName : 'no merged name');
$localMergedGone = $mergedName === '' || ! is_file($uploadDir . $mergedName);
$this->step('MERGE-06', $localMergedGone, $mergedName !== '' ? basename($mergedName) : 'n/a');
$rowOk = false;
if ($mergedId) {
$row = $claimFiles->find((int) $mergedId);
$rowOk = is_array($row)
&& (int) ($row['file_type'] ?? 0) === MERGED_CLAIM_FILE_TYPE
&& (int) ($row['is_active'] ?? 0) === 1
&& (string) ($row['file_name'] ?? '') === $mergedName;
}
$this->step('MERGE-07', $rowOk, 'merged_file_id=' . ($mergedId ?? 'null'));
// MERGE-08: no leftover smoke temps for our source names in claim_files_runtime
$afterRuntime = $this->listClaimRuntimeFiles();
$leaked = [];
foreach ($afterRuntime as $f) {
foreach ($srcNames as $src) {
if (strpos($f, $src) !== false) {
$leaked[] = $f;
}
}
}
$this->step('MERGE-08', $leaked === [], $leaked === [] ? 'no leaks' : implode(',', $leaked));
// MERGE-09 cleanup
$cleanOk = $this->cleanup(
$storage,
$uploadDir,
$srcNames,
$mergedName !== '' ? $mergedName : null,
$ticketId,
$sourceIds,
$keep,
$mergedId ? [(int) $mergedId] : []
);
if ($keep) {
$this->skip('MERGE-09', '--keep set; left ticket_id=' . $ticketId);
} else {
$srcGone = true;
foreach ($srcNames as $name) {
if ($storage->exists(rtrim($uploadDir, '/\\'), $name)) {
$srcGone = false;
}
}
$mergedGone = $mergedName === '' || ! $storage->exists(rtrim($uploadDir, '/\\'), $mergedName);
$this->step('MERGE-09', $cleanOk && $srcGone && $mergedGone, 'ticket_id=' . $ticketId);
}
} catch (\Throwable $e) {
CLI::error('Unhandled: ' . $e->getMessage());
$this->fail++;
$this->cleanup($storage, $uploadDir, $srcNames, $mergedName, $ticketId, $sourceIds, $keep, $mergedId ? [(int) $mergedId] : []);
}
$this->printResult();
return $this->fail > 0 ? EXIT_ERROR : EXIT_SUCCESS;
}
private function makeOnePagePdf(string $uploadDir, string $fileName, string $text): ?string
{
$tempDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
if (! is_dir($tempDir)) {
@mkdir($tempDir, 0775, true);
}
$path = $uploadDir . $fileName;
try {
$mpdf = new Mpdf(['tempDir' => $tempDir, 'mode' => 'utf-8']);
$mpdf->WriteHTML('<h1>' . htmlspecialchars($text, ENT_QUOTES, 'UTF-8') . '</h1><p>storage merge smoke</p>');
$mpdf->Output($path, Destination::FILE);
return is_file($path) ? $path : null;
} catch (\Throwable $e) {
CLI::error('PDF create failed: ' . $e->getMessage());
return null;
}
}
/** @return list<string> */
private function listClaimRuntimeFiles(): array
{
$dir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR
. 'cache' . DIRECTORY_SEPARATOR
. 'claim_files_runtime' . DIRECTORY_SEPARATOR;
if (! is_dir($dir)) {
return [];
}
$out = [];
foreach (scandir($dir) ?: [] as $f) {
if ($f === '.' || $f === '..') {
continue;
}
if (is_file($dir . $f)) {
$out[] = $f;
}
}
return $out;
}
/**
* @param list<string> $srcNames
* @param list<int> $sourceIds
* @param list<int> $extraIds
*/
private function cleanup(
$storage,
string $uploadDir,
array $srcNames,
?string $mergedName,
int $ticketId,
array $sourceIds,
bool $keep,
array $extraIds = []
): bool {
if ($keep) {
return true;
}
$ok = true;
$dir = rtrim($uploadDir, '/\\');
foreach ($srcNames as $name) {
if ($storage->exists($dir, $name)) {
$del = $storage->delete($dir, $name);
if (! ($del['success'] ?? false)) {
$ok = false;
}
}
@unlink($uploadDir . $name);
}
if ($mergedName) {
if ($storage->exists($dir, $mergedName)) {
$del = $storage->delete($dir, $mergedName);
if (! ($del['success'] ?? false)) {
$ok = false;
}
}
@unlink($uploadDir . $mergedName);
}
try {
$claimFiles = new ClaimFilesModel();
$ids = array_values(array_unique(array_merge($sourceIds, $extraIds)));
if ($ids !== []) {
$claimFiles->whereIn('id', $ids)->set(['is_active' => 0])->update();
}
$claimFiles->where('ticket_id', $ticketId)->set(['is_active' => 0])->update();
} catch (\Throwable $e) {
CLI::write('DB cleanup warning: ' . $e->getMessage(), 'yellow');
$ok = false;
}
return $ok;
}
private function step(string $id, bool $ok, string $detail = ''): void
{
$title = $this->titleFor($id);
if ($ok) {
$this->pass++;
CLI::write("PASS {$id} {$title}" . ($detail !== '' ? "{$detail}" : ''), 'green');
} else {
$this->fail++;
CLI::write("FAIL {$id} {$title}" . ($detail !== '' ? "{$detail}" : ''), 'red');
}
}
private function skip(string $id, string $reason): void
{
$this->skip++;
CLI::write("SKIP {$id} {$this->titleFor($id)}{$reason}", 'yellow');
}
private function titleFor(string $id): string
{
foreach ($this->catalog as $row) {
if ($row['id'] === $id) {
return $row['title'];
}
}
return $id;
}
private function printResult(): void
{
CLI::newLine();
CLI::write(
sprintf('MERGE RESULT: %d passed | %d failed | %d skipped', $this->pass, $this->fail, $this->skip),
$this->fail === 0 ? 'green' : 'red'
);
}
}

View File

@ -0,0 +1,189 @@
<?php
namespace App\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Purge objects under an S3 prefix that matches a storage module folder
* (e.g. claim_files keys claim_files/* in AWS_FILE_UPLOAD_BUCKET).
*
* Usage:
* php spark storage:purge-s3 --module claim_files
* php spark storage:purge-s3 --module claim_files --execute
* php spark storage:purge-s3 --module claim_files,excel --limit 100 --execute
*/
class StoragePurgeS3 extends BaseCommand
{
protected $group = 'Storage';
protected $name = 'storage:purge-s3';
protected $description = 'List/delete S3 objects under a module prefix (dry-run by default).';
protected $usage = 'storage:purge-s3 --module <folder>[,folder...] [--limit 0] [--offset 0] [--execute] [--list-modules]';
protected $options = [
'--module' => 'Required. Module folder name(s), comma-separated (e.g. claim_files).',
'--limit' => 'Max objects to delete per module (0 = all, default: 0).',
'--offset' => 'Skip first N keys per module (default: 0).',
'--execute' => 'Actually delete. Without this flag, dry-run only.',
'--list-modules' => 'List known local upload module folder names and exit.',
];
public function run(array $params)
{
if (CLI::getOption('list-modules') !== null) {
$this->printKnownModules();
return EXIT_SUCCESS;
}
$moduleOpt = trim((string) (CLI::getOption('module') ?? ($params[0] ?? '')));
if ($moduleOpt === '') {
CLI::error('Missing --module. Example: php spark storage:purge-s3 --module claim_files');
CLI::write('Tip: php spark storage:purge-s3 --list-modules');
return EXIT_ERROR;
}
$execute = CLI::getOption('execute') !== null;
$dryRun = ! $execute;
$limit = max(0, (int) (CLI::getOption('limit') ?? 0));
$offset = max(0, (int) (CLI::getOption('offset') ?? 0));
$modules = $this->parseModules($moduleOpt);
if ($modules === []) {
CLI::error('No valid module names after parsing. Use basename-only folder names (no path).');
return EXIT_ERROR;
}
$storage = \Config\Services::getFileStorageService(false);
if (! $storage->usesS3()) {
CLI::error('FileStorageService is not in S3 mode. Set FILE_STORAGE_DRIVER=s3 and AWS_FILE_UPLOAD_BUCKET.');
return EXIT_ERROR;
}
$s3 = \Config\Services::getS3Service(false);
$bucket = $storage->getBucket();
CLI::write($dryRun ? 'Running in DRY-RUN mode (no deletes).' : 'Running in EXECUTE mode (deletes enabled).', 'yellow');
CLI::write('Bucket: ' . ($bucket ?? 'N/A'), 'cyan');
CLI::write('Modules: ' . implode(', ', $modules));
CLI::newLine();
$totals = [
'listed' => 0,
'deleted' => 0,
'failed' => 0,
];
foreach ($modules as $module) {
$prefix = $module . '/';
CLI::write("Module: {$module}", 'green');
CLI::write(" Prefix: {$prefix}");
$listed = $s3->listFiles($prefix, $bucket);
if (! ($listed['success'] ?? false)) {
CLI::error(' List failed: ' . ($listed['message'] ?? 'unknown'));
$totals['failed']++;
CLI::newLine();
continue;
}
$keys = [];
foreach ($listed['files'] ?? [] as $file) {
$key = (string) ($file['key'] ?? '');
if ($key === '' || substr($key, -1) === '/') {
continue; // skip empty / folder markers
}
$keys[] = $key;
}
sort($keys);
$batch = array_slice($keys, $offset, $limit > 0 ? $limit : null);
CLI::write(' Objects under prefix: ' . count($keys));
CLI::write(' In this batch: ' . count($batch));
if ($batch === []) {
CLI::write(' Nothing to purge.', 'yellow');
CLI::newLine();
continue;
}
foreach ($batch as $key) {
$totals['listed']++;
if ($dryRun) {
CLI::write(" - [dry-run] Would delete: {$key}");
continue;
}
$result = $s3->delete($key, $bucket);
if ($result['success'] ?? false) {
$totals['deleted']++;
CLI::write(" - Deleted: {$key}", 'green');
} else {
$totals['failed']++;
CLI::write(' - Failed: ' . $key . ' | ' . ($result['message'] ?? ''), 'red');
}
}
CLI::newLine();
}
CLI::write('Purge summary', 'cyan');
CLI::write(' Listed (batch): ' . $totals['listed']);
CLI::write(' Deleted: ' . $totals['deleted'], $totals['deleted'] > 0 ? 'green' : 'white');
CLI::write(' Failed: ' . $totals['failed'], $totals['failed'] > 0 ? 'red' : 'white');
if ($dryRun) {
CLI::newLine();
CLI::write('Dry-run complete. Re-run with --execute to delete these objects.', 'yellow');
}
return $totals['failed'] > 0 ? EXIT_ERROR : EXIT_SUCCESS;
}
/**
* @return list<string>
*/
private function parseModules(string $moduleOpt): array
{
$raw = array_filter(array_map('trim', explode(',', strtolower($moduleOpt))));
$out = [];
foreach ($raw as $module) {
$module = basename(str_replace('\\', '/', $module));
if ($module === '' || $module === '.' || $module === '..') {
continue;
}
if (strpos($module, '/') !== false || preg_match('/[^a-z0-9_\-]/i', $module)) {
CLI::write(" Skipping invalid module name: {$module}", 'yellow');
continue;
}
$out[] = $module;
}
return array_values(array_unique($out));
}
private function printKnownModules(): void
{
$uploadRoot = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads';
CLI::write('Known local upload folders (S3 prefix = folder name):', 'cyan');
$entries = @scandir($uploadRoot);
if (! is_array($entries)) {
CLI::write(' (uploads root missing)');
return;
}
$names = [];
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
if (is_dir($uploadRoot . DIRECTORY_SEPARATOR . $entry)) {
$names[] = $entry;
}
}
sort($names);
foreach ($names as $name) {
CLI::write(' ' . $name);
}
CLI::newLine();
CLI::write('Example: php spark storage:purge-s3 --module claim_files');
}
}

View File

@ -0,0 +1,514 @@
<?php
namespace App\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Runs ALL automatable S3/storage scenarios in one command.
*
* Usage:
* php spark storage:smoke-test
* php spark storage:smoke-test --module excel
* php spark storage:smoke-test --list # list all scenarios without running
*/
class StorageSmokeTest extends BaseCommand
{
protected $group = 'Storage';
protected $name = 'storage:smoke-test';
protected $description = 'Run every automatable storage scenario (all modules) and list remaining manual ones.';
protected $usage = 'storage:smoke-test [--module all|excel|claim_files|...] [--list]';
protected $options = [
'--module' => 'One folder name, or all (default: all wired folders).',
'--list' => 'List every scenario ID without executing.',
];
private int $pass = 0;
private int $fail = 0;
private int $skip = 0;
/** @var list<array{id:string,title:string,group:string}> */
private array $catalog = [];
/** @var list<string> */
private array $wiredModules = [
'claim_files',
'client_kyc_documents',
'excel',
'import_excel',
'lead_files',
'attachments',
'claim_dump_excel',
'claims_mis',
'claims_dump',
'statements',
'bds_dump_excel',
'hr_files',
'non_eb_asset_files',
'non_eb_rack_rate',
];
public function run(array $params)
{
helper('utility');
$this->buildCatalog();
if (CLI::getOption('list') !== null) {
$this->printCatalog();
return EXIT_SUCCESS;
}
$storage = \Config\Services::getFileStorageService(false);
$moduleOpt = strtolower((string) (CLI::getOption('module') ?? 'all'));
$modules = $moduleOpt === 'all' || $moduleOpt === ''
? $this->wiredModules
: [basename($moduleOpt)];
CLI::write('========== STORAGE FULL SCENARIO RUN ==========', 'cyan');
CLI::write('usesS3: ' . ($storage->usesS3() ? 'YES' : 'NO'));
CLI::write('Bucket: ' . ($storage->getBucket() ?? 'N/A'));
CLI::write('RETAIN_LOCAL: ' . (storage_env_retain_local() ? 'true' : 'false'));
CLI::write('Modules: ' . implode(', ', $modules));
CLI::write('Total catalog scenarios: ' . count($this->catalog));
CLI::newLine();
// ENV / helper checks
$this->runEnvScenarios($storage);
// Per-module full lifecycle for every wired folder
foreach ($modules as $mod) {
$this->runModuleLifecycle($storage, $mod);
}
// Cross-cutting claim / mail / mirror / soft-delete
$this->runClaimScenarios($storage);
$this->runMailScenarios($storage);
$this->runMirrorScenario($storage);
$this->runRetainLocalSimulation($storage);
$this->runClaimsDumpResolve($storage);
CLI::newLine();
CLI::write(
sprintf('RESULT: %d passed | %d failed | %d skipped', $this->pass, $this->fail, $this->skip),
$this->fail === 0 ? 'green' : 'red'
);
CLI::newLine();
$this->printManualOnlyRemaining();
return $this->fail > 0 ? EXIT_ERROR : EXIT_SUCCESS;
}
private function buildCatalog(): void
{
$this->catalog = [
['id' => 'ENV-01', 'group' => 'ENV', 'title' => 'FILE_STORAGE / bucket / RETAIN_LOCAL readable'],
['id' => 'ENV-02', 'group' => 'ENV', 'title' => 'Runtime temp dirs writable'],
['id' => 'ENV-03', 'group' => 'ENV', 'title' => 'usesS3 matches FILE_STORAGE_DRIVER'],
];
foreach ($this->wiredModules as $mod) {
$this->catalog[] = ['id' => "MOD-{$mod}-01", 'group' => "MODULE:{$mod}", 'title' => 'Upload to S3/local'];
$this->catalog[] = ['id' => "MOD-{$mod}-02", 'group' => "MODULE:{$mod}", 'title' => 'Exists after upload'];
$this->catalog[] = ['id' => "MOD-{$mod}-03", 'group' => "MODULE:{$mod}", 'title' => 'Download content'];
$this->catalog[] = ['id' => "MOD-{$mod}-04", 'group' => "MODULE:{$mod}", 'title' => 'ensure_local from storage'];
$this->catalog[] = ['id' => "MOD-{$mod}-05", 'group' => "MODULE:{$mod}", 'title' => 'No permanent local when RETAIN_LOCAL=false (S3)'];
$this->catalog[] = ['id' => "MOD-{$mod}-06", 'group' => "MODULE:{$mod}", 'title' => 'Delete removes object'];
$this->catalog[] = ['id' => "MOD-{$mod}-07", 'group' => "MODULE:{$mod}", 'title' => 'Presigned URL (S3 only)'];
}
$extra = [
['id' => 'CLAIM-01', 'group' => 'CLAIM', 'title' => 'resolve_claim_file_path'],
['id' => 'CLAIM-02', 'group' => 'CLAIM', 'title' => 'claim_file_download_url (TPA)'],
['id' => 'CLAIM-03', 'group' => 'CLAIM', 'title' => 'soft_delete cleans S3 when unused (DB)'],
['id' => 'MERGE-PDF', 'group' => 'CLAIM', 'title' => 'Dedicated PDF merge suite — php spark storage:smoke-test-merge-pdf'],
['id' => 'MAIL-01', 'group' => 'MAIL', 'title' => 'resolve_mail_attachment_path'],
['id' => 'GEN-01', 'group' => 'GENERATED', 'title' => 'mirror_generated_file then local gone'],
['id' => 'RETAIN-01', 'group' => 'RETAIN_LOCAL', 'title' => 'Simulate retain=false (S3-only)'],
['id' => 'RETAIN-02', 'group' => 'RETAIN_LOCAL', 'title' => 'Simulate retain=true (dual-write keep local)'],
['id' => 'DUMP-01', 'group' => 'CLAIMS_DUMP', 'title' => 'resolveDumpLocalPath / ensure_local'],
];
foreach ($extra as $row) {
$this->catalog[] = $row;
}
}
private function printCatalog(): void
{
CLI::write('========== ALL SCENARIO TEST CASES ==========', 'cyan');
$current = '';
foreach ($this->catalog as $row) {
if ($row['group'] !== $current) {
$current = $row['group'];
CLI::newLine();
CLI::write("--- {$current} ---", 'yellow');
}
CLI::write(" {$row['id']} {$row['title']}");
}
CLI::newLine();
CLI::write('Manual-only (UI/TPA jobs — run by human after automated):', 'yellow');
foreach ($this->manualScenarioLines() as $line) {
CLI::write(' ' . $line);
}
CLI::newLine();
CLI::write('Run all automated: php spark storage:smoke-test');
}
private function step(string $id, bool $ok, string $detail = ''): void
{
$title = $this->titleFor($id);
if ($ok) {
$this->pass++;
CLI::write("[PASS] {$id} {$title}" . ($detail !== '' ? "{$detail}" : ''), 'green');
} else {
$this->fail++;
CLI::write("[FAIL] {$id} {$title}" . ($detail !== '' ? "{$detail}" : ''), 'red');
}
}
private function skip(string $id, string $reason): void
{
$this->skip++;
CLI::write("[SKIP] {$id} {$this->titleFor($id)}{$reason}", 'yellow');
}
private function titleFor(string $id): string
{
foreach ($this->catalog as $row) {
if ($row['id'] === $id) {
return $row['title'];
}
}
return $id;
}
private function uploadPath(string $mod): string
{
return rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . $mod;
}
private function makeTmp(string $name, string $body): string
{
$dir = storage_runtime_temp_dir();
if (! is_dir($dir)) {
mkdir($dir, 0755, true);
}
$path = $dir . DIRECTORY_SEPARATOR . $name;
file_put_contents($path, $body);
return $path;
}
private function runEnvScenarios($storage): void
{
CLI::write('--- ENV ---', 'white');
$driver = strtolower((string) (getenv('FILE_STORAGE_DRIVER') ?: env('FILE_STORAGE_DRIVER') ?: ''));
$bucket = (string) (getenv('AWS_FILE_UPLOAD_BUCKET') ?: getenv('AWS_BUCKET') ?: '');
$this->step('ENV-01', true, "driver=" . ($driver !== '' ? $driver : 'default') . ", retain=" . (storage_env_retain_local() ? '1' : '0') . ", bucket=" . ($bucket !== '' ? 'set' : 'empty'));
$runtime = storage_runtime_temp_dir();
$claimRt = rtrim(WRITEPATH, '/\\') . '/cache/claim_files_runtime';
foreach ([$runtime, $claimRt] as $d) {
if (! is_dir($d)) {
@mkdir($d, 0755, true);
}
}
$probe = $runtime . DIRECTORY_SEPARATOR . 'probe_' . uniqid('', true) . '.txt';
$wrote = @file_put_contents($probe, 'ok') !== false;
if ($wrote) {
@unlink($probe);
}
$this->step('ENV-02', $wrote && is_dir($runtime));
if ($driver === 'local') {
$this->step('ENV-03', ! $storage->usesS3(), 'local driver => usesS3 false');
} elseif ($driver === 's3' || $driver === '') {
$this->step('ENV-03', $storage->usesS3(), 's3 driver => usesS3 true (bucket required)');
} else {
$this->skip('ENV-03', 'unknown FILE_STORAGE_DRIVER=' . $driver);
}
}
private function runModuleLifecycle($storage, string $mod): void
{
CLI::write("--- MODULE: {$mod} ---", 'white');
$path = $this->uploadPath($mod);
$fileName = 'smoke_' . $mod . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(2)) . '.txt';
$tmp = $this->makeTmp($fileName, "smoke-{$mod}\n" . date('c') . "\n");
$up = $storage->upload($tmp, $path, $fileName);
@unlink($tmp);
$this->step("MOD-{$mod}-01", (bool) ($up['success'] ?? false), (string) ($up['message'] ?? ''));
if (! ($up['success'] ?? false)) {
// Skip rest of module if upload failed
foreach (['02', '03', '04', '05', '06', '07'] as $n) {
$this->skip("MOD-{$mod}-{$n}", 'upload failed');
}
return;
}
$this->step("MOD-{$mod}-02", $storage->exists($path, $fileName));
$dl = $storage->download($path, $fileName);
$contentOk = ($dl['success'] ?? false) && (
((! empty($dl['content'])) && str_contains((string) $dl['content'], "smoke-{$mod}"))
|| ((! empty($dl['path'])) && is_file((string) $dl['path'])
&& str_contains((string) file_get_contents((string) $dl['path']), "smoke-{$mod}"))
);
$this->step("MOD-{$mod}-03", $contentOk, (string) ($dl['message'] ?? ''));
// Force missing permanent local then ensure_local
$permanent = $storage->resolveLocalPath($path, $fileName);
if (is_file($permanent)) {
@unlink($permanent);
}
$ensured = storage_ensure_local_file($path, $fileName);
$this->step(
"MOD-{$mod}-04",
is_string($ensured) && is_file($ensured) && str_contains((string) file_get_contents($ensured), "smoke-{$mod}"),
$ensured ? basename($ensured) : 'null'
);
if ($ensured) {
storage_cleanup_temp_file($ensured);
}
if ($storage->usesS3() && ! storage_env_retain_local()) {
$this->step("MOD-{$mod}-05", ! is_file($permanent), 'permanent path absent');
} else {
$this->skip("MOD-{$mod}-05", $storage->usesS3() ? 'RETAIN_LOCAL=true' : 'local driver');
}
$del = $storage->delete($path, $fileName);
$this->step("MOD-{$mod}-06", (bool) ($del['success'] ?? false) && ! $storage->exists($path, $fileName));
if ($storage->usesS3()) {
$tmp = $this->makeTmp($fileName, "presign-{$mod}\n");
$storage->upload($tmp, $path, $fileName);
@unlink($tmp);
$pre = $storage->getPresignedUrl($path, $fileName, 5);
$this->step("MOD-{$mod}-07", (bool) ($pre['success'] ?? false) && ! empty($pre['url']));
$storage->delete($path, $fileName);
} else {
$this->skip("MOD-{$mod}-07", 'local driver');
}
}
private function runClaimScenarios($storage): void
{
CLI::write('--- CLAIM ---', 'white');
$claimDir = $this->uploadPath('claim_files');
$fileName = 'smoke_claim_' . date('Ymd_His') . '_' . bin2hex(random_bytes(2)) . '.pdf';
$tmp = $this->makeTmp($fileName, "%PDF-1.4 smoke-claim\n");
$up = $storage->upload($tmp, $claimDir, $fileName);
@unlink($tmp);
if (! ($up['success'] ?? false)) {
$this->skip('CLAIM-01', 'upload claim sample failed');
$this->skip('CLAIM-02', 'upload claim sample failed');
$this->skip('CLAIM-03', 'upload claim sample failed');
return;
}
// Ensure no permanent leftover interferes
$permanent = $storage->resolveLocalPath($claimDir, $fileName);
if (is_file($permanent)) {
@unlink($permanent);
}
$resolved = storage_resolve_claim_file_path($claimDir, $fileName);
$this->step(
'CLAIM-01',
(bool) ($resolved['success'] ?? false) && ! empty($resolved['path']) && is_file((string) $resolved['path']),
(string) ($resolved['message'] ?? '')
);
storage_cleanup_temp_claim_file($resolved);
if ($storage->usesS3()) {
$url = storage_claim_file_download_url($fileName, null, 10);
$this->step('CLAIM-02', is_string($url) && str_starts_with($url, 'http'));
} else {
$this->skip('CLAIM-02', 'local driver');
}
// Soft-delete: insert temp claim_files row, soft delete, expect S3 gone
try {
$db = \Config\Database::connect();
$insert = [
'ticket_master_id' => 0,
'file_name' => $fileName,
'url' => $fileName,
'doc_name' => 'smoke-test',
'file_type' => 2,
'is_active' => 1,
'created_at' => date('Y-m-d H:i:s'),
];
// Only use columns that exist
$fields = $db->getFieldNames('claim_files');
$row = [];
foreach ($insert as $k => $v) {
if (in_array($k, $fields, true)) {
$row[$k] = $v;
}
}
$db->table('claim_files')->insert($row);
$id = (int) $db->insertID();
$ok = $id > 0 && storage_soft_delete_claim_file($id);
$gone = ! $storage->exists($claimDir, $fileName);
$this->step('CLAIM-03', $ok && $gone, 'id=' . $id);
// Hard cleanup leftover row
if ($id > 0) {
$db->table('claim_files')->where('id', $id)->delete();
}
} catch (\Throwable $e) {
$this->skip('CLAIM-03', 'DB soft-delete unavailable: ' . $e->getMessage());
$storage->delete($claimDir, $fileName);
}
}
private function runMailScenarios($storage): void
{
CLI::write('--- MAIL ---', 'white');
$dir = $this->uploadPath('attachments');
$fileName = 'smoke_mail_' . date('Ymd_His') . '_' . bin2hex(random_bytes(2)) . '.txt';
$tmp = $this->makeTmp($fileName, "mail\n");
$up = $storage->upload($tmp, $dir, $fileName);
@unlink($tmp);
if (! ($up['success'] ?? false)) {
$this->step('MAIL-01', false, (string) ($up['message'] ?? ''));
return;
}
$permanent = $storage->resolveLocalPath($dir, $fileName);
if (is_file($permanent)) {
@unlink($permanent);
}
$resolved = storage_resolve_mail_attachment_path('uploads/attachments/' . $fileName, $fileName);
$this->step(
'MAIL-01',
(bool) ($resolved['success'] ?? false) && ! empty($resolved['path']) && is_file((string) $resolved['path'])
);
if (! empty($resolved['path'])) {
storage_cleanup_temp_file((string) $resolved['path']);
}
$storage->delete($dir, $fileName);
}
private function runMirrorScenario($storage): void
{
CLI::write('--- GENERATED ---', 'white');
$dir = $this->uploadPath('excel');
$fileName = 'smoke_gen_' . date('Ymd_His') . '_' . bin2hex(random_bytes(2)) . '.txt';
$local = $this->makeTmp($fileName, "generated\n");
$ok = storage_mirror_generated_file($local, $dir, $fileName);
if ($storage->usesS3()) {
$this->step('GEN-01', $ok && ! is_file($local) && $storage->exists($dir, $fileName));
$storage->delete($dir, $fileName);
} else {
$this->step('GEN-01', $ok && is_file($local));
@unlink($local);
}
}
private function runRetainLocalSimulation($storage): void
{
CLI::write('--- RETAIN_LOCAL simulation ---', 'white');
if (! $storage->usesS3()) {
$this->skip('RETAIN-01', 'local driver');
$this->skip('RETAIN-02', 'local driver');
return;
}
$dir = $this->uploadPath('excel');
// retain=false behaviour: upload via temp then unlink (current default path)
$f1 = 'smoke_retain_off_' . bin2hex(random_bytes(2)) . '.txt';
$t1 = $this->makeTmp($f1, "off\n");
$storage->upload($t1, $dir, $f1);
@unlink($t1);
$p1 = $storage->resolveLocalPath($dir, $f1);
$this->step('RETAIN-01', ! is_file($p1) && $storage->exists($dir, $f1));
$storage->delete($dir, $f1);
// retain=true behaviour: keep local file after S3 upload
$f2 = 'smoke_retain_on_' . bin2hex(random_bytes(2)) . '.txt';
// Use runtime path as the "kept local" copy (CLI may lack write perms under uploads/)
$p2 = $this->makeTmp($f2, "on\n");
$up2 = $storage->upload($p2, $dir, $f2);
$this->step(
'RETAIN-02',
(bool) ($up2['success'] ?? false) && is_file($p2) && $storage->exists($dir, $f2),
'local kept + S3'
);
@unlink($p2);
$storage->delete($dir, $f2);
}
private function runClaimsDumpResolve($storage): void
{
CLI::write('--- CLAIMS_DUMP ---', 'white');
$dir = $this->uploadPath('claims_dump');
$fileName = 'smoke_cdump_' . date('Ymd_His') . '_' . bin2hex(random_bytes(2)) . '.xlsx';
$tmp = $this->makeTmp($fileName, "claims-dump\n");
$up = $storage->upload($tmp, $dir, $fileName);
@unlink($tmp);
if (! ($up['success'] ?? false)) {
$this->step('DUMP-01', false, (string) ($up['message'] ?? ''));
return;
}
$permanent = $storage->resolveLocalPath($dir, $fileName);
if (is_file($permanent)) {
@unlink($permanent);
}
$resolved = storage_ensure_local_file($dir, $fileName);
$this->step('DUMP-01', is_string($resolved) && is_file($resolved));
if ($resolved) {
storage_cleanup_temp_file($resolved);
}
$storage->delete($dir, $fileName);
}
/** @return list<string> */
private function manualScenarioLines(): array
{
return [
'UI-KYC-01 Upload KYC in browser',
'UI-KYC-02 Download download-kyc-docs/{id}',
'UI-KYC-03 Re-upload / delete KYC',
'UI-PT-01 Upload PT doc + download-pt-docs/{id}',
'UI-PT-02 REST downloadPolicyFiles',
'UI-CLAIM-01 Upload claim file in ticket UI',
'UI-CLAIM-02 downloadClaimFile / viewClaimFile',
'UI-CLAIM-03 Remove claim file in UI',
'UI-CLAIM-04 Merge PDFs in UI (core automated: php spark storage:smoke-test-merge-pdf)',
'UI-TPA-01 FHPL claim submit (base64)',
'UI-TPA-02 Vidal claim submit (base64)',
'UI-TPA-03 HealthIndia claim submit',
'UI-TPA-04 MediAssist uses presigned URL',
'UI-TPA-05 Volo uses presigned URL',
'UI-EMP-01 Employee excel upload + background job',
'UI-EMP-02 download-file-list / download_import_file',
'UI-EMP-03 HR upload + hrFileDownload',
'UI-LEAD-01 Lead member upload + demography job',
'UI-LEAD-02 downloadMemberFile / error excel',
'UI-DUMP-01 Claim dump / MIS / statements / BDS UI jobs',
'UI-MAIL-01 Compose mail with attachment send',
'UI-NEB-01 Non-EB asset + rack rate UI',
'UI-NEG-01 Invalid extension rejected in UI',
'UI-NEG-02 Toggle FILE_STORAGE_DRIVER=local and retest one upload',
'UI-NEG-03 Toggle RETAIN_LOCAL=true/false and retest one upload',
];
}
private function printManualOnlyRemaining(): void
{
CLI::write('========== MANUAL UI / TPA / JOB (not auto-runnable) ==========', 'yellow');
CLI::write('These need browser login / real TPA — tick after you test:');
foreach ($this->manualScenarioLines() as $i => $line) {
CLI::write(sprintf(' [ ] %s', $line));
}
CLI::newLine();
CLI::write('Commands:');
CLI::write(' php spark storage:smoke-test # run ALL automated scenarios');
CLI::write(' php spark storage:smoke-test --list # list every scenario ID');
CLI::write(' php spark storage:smoke-test --module claim_files');
}
}

View File

@ -0,0 +1,413 @@
<?php
namespace App\Commands;
use App\Libraries\TPAClaimsImportServices\IciciClaimImportService;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Temporary dry-run helper for ICICI existing-ticket status update logic.
*
* Usage:
* php spark tpa:test-icici-status --scenario
* php spark tpa:test-icici-status --scenario --apply
* php spark tpa:test-icici-status 68
* php spark tpa:test-icici-status 68 --apply
*/
class TestIciciStatusUpdate extends BaseCommand
{
protected $group = 'TPA';
protected $name = 'tpa:test-icici-status';
protected $description = 'Dry-run / apply ICICI existing ticket status update logic';
protected $usage = 'tpa:test-icici-status [file_id] [--scenario] [--apply] [--new-status STATUS]';
protected $arguments = [
'file_id' => 'Optional claim_dump_files.id to process',
];
protected $options = [
'--scenario' => 'Seed a controlled pending dump row for one existing ICICI ticket and test status update',
'--apply' => 'Actually run runTicketMasterInsert (default is map-only dry-run)',
'--new-status' => 'Dump status string to use in scenario (default: REJECTED)',
];
public function run(array $params)
{
helper('utility_helper');
$db = db_connect();
$iciciTpaId = (int) env('ICICI_PRIMARY_KEY_CONSTANT');
$fileId = $this->resolveFileId($params);
$apply = $this->hasFlag('apply');
$scenario = $this->hasFlag('scenario');
$newStatus = $this->resolveOptionValue('new-status', 'REJECTED');
CLI::write("ICICI_PRIMARY_KEY_CONSTANT = {$iciciTpaId}", 'yellow');
CLI::write('Parsed args: file_id=' . $fileId . ', apply=' . ($apply ? 'yes' : 'no') . ', scenario=' . ($scenario ? 'yes' : 'no') . ', new_status=' . $newStatus, 'yellow');
if ($scenario) {
$fileId = $this->seedScenario($db, $iciciTpaId, $newStatus);
if ($fileId <= 0) {
return;
}
}
if ($fileId <= 0) {
$recent = $db->table('claim_dump_files')
->select('id, tpa_id, client_id, client_policy_id, file_name, status, created_at')
->where('tpa_id', $iciciTpaId)
->orderBy('id', 'DESC')
->limit(10)
->get()
->getResultArray();
CLI::write('No --file_id provided. Recent ICICI claim_dump_files:', 'cyan');
if (empty($recent)) {
CLI::error('No ICICI claim dump files found.');
$this->printMatchPreview($db, $iciciTpaId);
return;
}
foreach ($recent as $row) {
CLI::write(json_encode($row));
}
CLI::newLine();
CLI::write('Controlled scenario test:', 'green');
CLI::write(' php spark tpa:test-icici-status --scenario');
CLI::write(' php spark tpa:test-icici-status --scenario --apply');
CLI::write('Existing file (use positional file_id):');
CLI::write(' php spark tpa:test-icici-status 68');
CLI::write(' php spark tpa:test-icici-status 68 --apply');
$this->printMatchPreview($db, $iciciTpaId);
return;
}
$file = $db->table('claim_dump_files')->where('id', $fileId)->get()->getRowArray();
if (empty($file)) {
CLI::error("claim_dump_files id={$fileId} not found");
return;
}
CLI::write('File: ' . json_encode([
'id' => $file['id'],
'tpa_id' => $file['tpa_id'],
'status' => $file['status'],
'file_name' => $file['file_name'],
'client_policy_id' => $file['client_policy_id'],
]), 'cyan');
if ((int) $file['tpa_id'] !== $iciciTpaId) {
CLI::error("File tpa_id={$file['tpa_id']} is not ICICI ({$iciciTpaId}). Aborting.");
return;
}
$pendingDump = $db->table('claims_dump_icici')
->where('file_id', $fileId)
->where('is_active', 1)
->where('ticket_id IS NULL', null, false)
->where('master_reject_reason IS NULL', null, false)
->countAllResults();
CLI::write("Pending dump rows for mapClaimMasterData: {$pendingDump}", 'yellow');
$service = new IciciClaimImportService();
if ($apply) {
CLI::write('APPLY mode: running runTicketMasterInsert...', 'light_red');
$pendingBefore = $db->table('claims_dump_icici')
->select('id, ticket_id, master_reject_reason, updated_status, employee_member_id, uhid, claimed_amount, doa')
->where('file_id', $fileId)
->where('is_active', 1)
->get()
->getResultArray();
CLI::write('Dump rows BEFORE: ' . json_encode($pendingBefore, JSON_PRETTY_PRINT));
$result = $service->runTicketMasterInsert(['file_id' => $fileId]);
CLI::write('Result: ' . json_encode($result, JSON_PRETTY_PRINT));
$pendingAfter = $db->table('claims_dump_icici')
->select('id, ticket_id, master_reject_reason, updated_status, employee_member_id, uhid, claimed_amount, doa')
->where('file_id', $fileId)
->where('is_active', 1)
->get()
->getResultArray();
CLI::write('Dump rows AFTER: ' . json_encode($pendingAfter, JSON_PRETTY_PRINT));
$ticketIds = array_values(array_filter(array_column($pendingAfter, 'ticket_id')));
if (!empty($ticketIds)) {
$ticketsAfter = $db->table('ticket_master')
->select('id, claim_status_id, claim_dump_ref_id, emp_code, tpa_no, claim_amount, doa')
->whereIn('id', $ticketIds)
->get()
->getResultArray();
CLI::write('Linked tickets AFTER: ' . json_encode($ticketsAfter, JSON_PRETTY_PRINT));
}
return;
}
CLI::write('DRY-RUN mode: calling mapClaimMasterData only (no DB writes)...', 'green');
$ref = new \ReflectionClass($service);
$method = $ref->getMethod('mapClaimMasterData');
$method->setAccessible(true);
$mapped = $method->invoke($service, $fileId);
if (empty($mapped['status'])) {
CLI::error('mapClaimMasterData failed: ' . ($mapped['message'] ?? 'unknown'));
if (!empty($mapped['error_data'])) {
CLI::write(json_encode($mapped['error_data'], JSON_PRETTY_PRINT));
}
return;
}
$inserts = $mapped['mapped_array'] ?? [];
$rejects = $mapped['rejected_reason_array'] ?? [];
$updates = $mapped['status_update_array'] ?? [];
CLI::newLine();
CLI::write('=== RESULT COUNTS ===', 'yellow');
CLI::write('New tickets to insert : ' . count($inserts));
CLI::write('Status updates (existing) : ' . count($updates));
CLI::write('Rejected / skipped rows : ' . count($rejects));
CLI::newLine();
CLI::write('=== STATUS UPDATE ARRAY ===', 'cyan');
CLI::write(empty($updates) ? '(none)' : json_encode($updates, JSON_PRETTY_PRINT));
CLI::newLine();
CLI::write('=== REJECT / SKIP REASONS (first 20) ===', 'cyan');
CLI::write(json_encode(array_slice($rejects, 0, 20), JSON_PRETTY_PRINT));
if (!empty($updates)) {
CLI::newLine();
CLI::write('Ticket fields BEFORE update:', 'yellow');
$ids = array_column($updates, 'id');
$before = $db->table('ticket_master')
->select('id, emp_code, tpa_no, claim_amount, doa, claim_status_id, claim_dump_ref_id, file_id')
->whereIn('id', $ids)
->get()
->getResultArray();
CLI::write(json_encode($before, JSON_PRETTY_PRINT));
}
CLI::newLine();
CLI::write('To apply for real:', 'green');
CLI::write(" php spark tpa:test-icici-status {$fileId} --apply");
}
private function resolveFileId(array $params): int
{
if (!empty($params['file_id'])) {
return (int) $params['file_id'];
}
if (!empty($params[0]) && is_numeric($params[0])) {
return (int) $params[0];
}
// Fallback for --file_id=68 / --file_id 68 (CI option parsing can miss these)
$argv = $_SERVER['argv'] ?? [];
foreach ($argv as $i => $arg) {
if (preg_match('/^--file[_-]id=(.+)$/', (string) $arg, $m)) {
return (int) $m[1];
}
if (in_array($arg, ['--file_id', '--file-id'], true) && isset($argv[$i + 1])) {
return (int) $argv[$i + 1];
}
}
$opt = CLI::getOption('file_id') ?? CLI::getOption('file-id');
return (int) ($opt ?? 0);
}
private function hasFlag(string $name): bool
{
if (CLI::getOption($name) !== null) {
return true;
}
$argv = $_SERVER['argv'] ?? [];
return in_array('--' . $name, $argv, true);
}
private function resolveOptionValue(string $name, string $default): string
{
$opt = CLI::getOption($name);
if (is_string($opt) && $opt !== '') {
return $opt;
}
$argv = $_SERVER['argv'] ?? [];
foreach ($argv as $i => $arg) {
if (preg_match('/^--' . preg_quote($name, '/') . '=(.+)$/', (string) $arg, $m)) {
return trim($m[1]);
}
if ($arg === '--' . $name && isset($argv[$i + 1]) && strpos((string) $argv[$i + 1], '--') !== 0) {
return (string) $argv[$i + 1];
}
}
return $default;
}
/**
* Create a temporary claim_dump_files + one pending claims_dump_icici row
* matching an existing ticket, with a different dump status.
*/
private function seedScenario($db, int $iciciTpaId, string $newStatus): int
{
CLI::write("Seeding controlled ICICI status-update scenario (new_status={$newStatus})...", 'cyan');
$ticket = $db->table('ticket_master')
->select('id, client_id, client_policy_id, emp_code, tpa_no, claim_amount, doa, claim_status_id, claim_dump_ref_id, tpa_id')
->where('tpa_id', $iciciTpaId)
->where('is_active', 1)
->where('emp_code IS NOT NULL', null, false)
->where('tpa_no IS NOT NULL', null, false)
->where('claim_amount IS NOT NULL', null, false)
->where('doa IS NOT NULL', null, false)
->orderBy('id', 'DESC')
->get()
->getRowArray();
if (empty($ticket)) {
// Fall back to any ticket, then override tpa matching by using that ticket's keys
$ticket = $db->table('ticket_master')
->select('id, client_id, client_policy_id, emp_code, tpa_no, claim_amount, doa, claim_status_id, claim_dump_ref_id, tpa_id')
->where('is_active', 1)
->where('claim_created_by', 'DUMP_TPA')
->where('emp_code IS NOT NULL', null, false)
->where('tpa_no IS NOT NULL', null, false)
->where('claim_amount IS NOT NULL', null, false)
->where('doa IS NOT NULL', null, false)
->orderBy('id', 'DESC')
->get()
->getRowArray();
}
if (empty($ticket)) {
CLI::error('No suitable existing ticket found to seed a status-update scenario.');
return 0;
}
CLI::write('Using existing ticket: ' . json_encode([
'id' => $ticket['id'],
'emp_code' => $ticket['emp_code'],
'tpa_no' => $ticket['tpa_no'],
'claim_amount' => $ticket['claim_amount'],
'doa' => $ticket['doa'],
'claim_status_id' => $ticket['claim_status_id'],
'claim_dump_ref_id' => $ticket['claim_dump_ref_id'] ?? null,
'tpa_id' => $ticket['tpa_id'],
]), 'yellow');
// Clear stale claim_dump_ref_id so the scenario can re-test link backfill.
if (!empty($ticket['claim_dump_ref_id'])) {
$db->table('ticket_master')->where('id', $ticket['id'])->update([
'claim_dump_ref_id' => null,
]);
$ticket['claim_dump_ref_id'] = null;
CLI::write('Cleared stale ticket.claim_dump_ref_id for retest.', 'yellow');
}
// Ensure a physical file exists for resolveTpaClaimDumpFile if --apply uses controller later
$uploadDir = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'claim_dump_excel' . DIRECTORY_SEPARATOR;
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$fileName = 'icici_status_test_' . date('Ymd_His') . '.xlsx';
$filePath = $uploadDir . $fileName;
if (!is_file($filePath)) {
file_put_contents($filePath, 'placeholder');
}
$fileInsert = [
'tpa_id' => $iciciTpaId,
'client_id' => $ticket['client_id'],
'client_policy_id' => $ticket['client_policy_id'],
'file_name' => $fileName,
'status' => 'pending',
'created_by' => 1,
'created_at' => date('Y-m-d H:i:s'),
'is_active' => 1,
];
// claim_dump_files may have different nullable columns; insert only known-safe ones
$fileCols = array_column($db->query('SHOW COLUMNS FROM claim_dump_files')->getResultArray(), 'Field');
$fileInsert = array_intersect_key($fileInsert, array_flip($fileCols));
$db->table('claim_dump_files')->insert($fileInsert);
$fileId = (int) $db->insertID();
if ($fileId <= 0) {
CLI::error('Failed to create claim_dump_files row for scenario.');
return 0;
}
$dumpInsert = [
'client_id' => $ticket['client_id'],
'client_policy_id' => $ticket['client_policy_id'],
'file_id' => $fileId,
'employee_member_id' => $ticket['emp_code'],
'uhid' => $ticket['tpa_no'],
'claimed_amount' => $ticket['claim_amount'],
'doa' => $ticket['doa'],
'updated_status' => $newStatus,
'relation' => 'SELF',
'is_active' => 1,
'created_by' => 1,
'created_at' => date('Y-m-d H:i:s'),
'ticket_id' => null,
'master_reject_reason' => null,
];
$dumpCols = array_column($db->query('SHOW COLUMNS FROM claims_dump_icici')->getResultArray(), 'Field');
$dumpInsert = array_intersect_key($dumpInsert, array_flip($dumpCols));
$db->table('claims_dump_icici')->insert($dumpInsert);
$dumpId = (int) $db->insertID();
CLI::write("Seeded file_id={$fileId}, dump_id={$dumpId}", 'green');
CLI::write("Expected: if ticket claim_status_id ({$ticket['claim_status_id']}) != mapped REJECTED(8)/chosen status, status_update_array should contain ticket {$ticket['id']}", 'green');
return $fileId;
}
private function printMatchPreview($db, int $iciciTpaId): void
{
CLI::newLine();
CLI::write('Preview: dump rows that match existing tickets (possible status updates)', 'cyan');
$sql = "
SELECT
cd.id AS dump_id,
cd.file_id,
cd.employee_member_id,
cd.uhid,
cd.claimed_amount,
cd.doa,
cd.updated_status AS dump_status,
tm.id AS ticket_id,
tm.claim_status_id AS current_status_id
FROM claims_dump_icici cd
INNER JOIN ticket_master tm
ON tm.is_active = 1
AND tm.emp_code = cd.employee_member_id
AND tm.tpa_no = cd.uhid
AND tm.claim_amount = cd.claimed_amount
AND tm.doa = cd.doa
WHERE cd.is_active = 1
AND tm.tpa_id = ?
ORDER BY cd.id DESC
LIMIT 10
";
$rows = $db->query($sql, [$iciciTpaId])->getResultArray();
if (empty($rows)) {
CLI::write('No overlapping dump/ticket pairs found.');
return;
}
CLI::write(json_encode($rows, JSON_PRETTY_PRINT));
}
}

View File

@ -0,0 +1,729 @@
<?php
namespace App\Commands;
use App\Libraries\TpaClaimsImportFactory;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use ReflectionClass;
/**
* End-to-end TPA claim dump test: generate Excel Job 1 Job 2.
*
* Usage:
* php spark tpa:e2e-pipeline --tpa=all
* php spark tpa:e2e-pipeline --tpa=icici --apply
* php spark tpa:e2e-pipeline --tpa-id=6 --apply
* php spark tpa:e2e-pipeline --tpa=all --apply --truncate
* php spark tpa:e2e-pipeline --tpa=icici --status-cases --apply
*/
class TestTpaE2ePipeline extends BaseCommand
{
protected $group = 'TPA';
protected $name = 'tpa:e2e-pipeline';
protected $description = 'Generate TPA Excel and run Job1 + Job2 end-to-end';
protected $usage = 'tpa:e2e-pipeline [--tpa=NAME|all] [--tpa-id=ID] [--apply] [--truncate] [--new-status=STATUS] [--status-cases]';
protected $options = [
'--tpa' => 'TPA key: icici|abhi|mediassist|fhpl|rcare|vidal|all (default: all)',
'--tpa-id' => 'Numeric TPA primary key (overrides --tpa)',
'--apply' => 'Run Job 1 + Job 2 (default is generate Excel + file row only)',
'--truncate' => 'Soft-truncate the test file after a successful apply',
'--new-status' => 'Override dump status string written into Excel',
'--status-cases' => 'Write 3 rows: empty status, unmapped status, mapped status (expect 61/61/mapped id)',
];
/**
* Per-TPA Excel seed config.
* excel_fields: excel header => source key from ticket/policy context
*/
private function tpaConfigs(): array
{
return [
'icici' => [
'env' => 'ICICI_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_icici',
'sheet' => null,
'default_status' => 'REJECTED',
'status_header' => 'Updated_status',
'status_db_column' => 'updated_status',
'mapped_status' => 'PAID',
'mapped_status_id' => 11,
'excel_fields' => [
'POLICY_NO' => 'policy_no',
'UHID' => 'tpa_no',
'EMPLOYEE_MEMBER_ID' => 'emp_code',
'RELATION' => 'relation',
'CLAIMED_AMOUNT' => 'claim_amount',
'DOA' => 'doa',
'Updated_status' => 'status_value',
],
],
'abhi' => [
'env' => 'ABHI_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_abhi',
'sheet' => null,
'default_status' => 'Rejected',
'status_header' => 'Claim Status',
'status_db_column' => 'claim_status',
'mapped_status' => 'Paid',
'mapped_status_id' => 11,
'excel_fields' => [
'Policy Number' => 'policy_no',
'HEALTHCARD_ID' => 'tpa_no',
'Member Code' => 'emp_code',
'Relation' => 'relation',
'Claimed Amount'=> 'claim_amount',
'DOA' => 'doa',
'Claim Status' => 'status_value',
],
],
'mediassist' => [
'env' => 'MEDI_ASSIST_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_medi_assist',
'sheet' => null,
'default_status' => 'Rejected',
'status_header' => 'claim_status',
'status_db_column' => 'claim_status',
'mapped_status' => 'Paid',
'mapped_status_id' => 11,
'excel_fields' => [
'policy_no' => 'policy_no',
'event_id' => 'tpa_no',
'pribenef_employee_code' => 'emp_code',
'benef_relation' => 'relation',
'claim_amount' => 'claim_amount',
'date_of_admission' => 'doa',
'claim_status' => 'status_value',
],
],
'fhpl' => [
'env' => 'FHPL_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_fhpl',
'sheet' => 'Claims&Preauth',
'default_status' => 'Rejected',
'status_header' => 'Current Claim Status',
'status_db_column' => 'current_claim_status',
'mapped_status' => 'Paid',
'mapped_status_id' => 11,
'excel_fields' => [
'Policy No' => 'policy_no',
'UHIDNO' => 'tpa_no',
'employeeid' => 'emp_code',
'relationship' => 'relation',
'claimamount' => 'claim_amount',
'Admdate' => 'doa',
'Current Claim Status' => 'status_value',
],
],
'rcare' => [
'env' => 'R_CARE_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_reliance',
'sheet' => 'CL',
'default_status' => 'Rejected',
'status_header' => 'Final Status',
'status_db_column' => 'final_status',
'mapped_status' => 'Paid',
'mapped_status_id' => 11,
'excel_fields' => [
'Policy Number' => 'policy_no',
'UHID' => 'tpa_no',
'Employee/Member Id' => 'emp_code',
'Relation' => 'relation',
'Claimed Amount' => 'claim_amount',
'DOA/OPD Treatment From' => 'doa',
'Final Status' => 'status_value',
],
],
'vidal' => [
'env' => 'VIDAL_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_vidal',
'sheet' => null,
'default_status' => 'Rejected',
'status_header' => 'Claim Status',
'status_db_column' => 'claim_status',
'mapped_status' => 'Paid',
'mapped_status_id' => 11,
'excel_fields' => [
'Insurer Policy Number' => 'policy_no',
'Primary Policy Holder Card ID' => 'tpa_no',
'Employee Number' => 'emp_code',
'Relation' => 'relation',
'Claim Amount' => 'claim_amount',
'Date of Admission' => 'doa',
'Claim Status' => 'status_value',
],
],
];
}
public function run(array $params)
{
helper('utility_helper');
$db = db_connect();
$apply = $this->hasFlag('apply');
$truncate = $this->hasFlag('truncate');
$statusCases = $this->hasFlag('status-cases');
$statusOverride = $this->resolveOptionValue('new-status', '');
$configs = $this->tpaConfigs();
$selected = $this->resolveSelectedTpas($configs);
if ($selected === []) {
return;
}
$summary = [];
foreach ($selected as $tpaKey) {
CLI::newLine();
CLI::write(str_repeat('=', 64), 'yellow');
CLI::write('E2E TPA: ' . strtoupper($tpaKey) . ($statusCases ? ' [status-cases]' : ''), 'yellow');
CLI::write(str_repeat('=', 64), 'yellow');
try {
$summary[$tpaKey] = $this->runForTpa(
$db,
$tpaKey,
$configs[$tpaKey],
$apply,
$truncate,
$statusOverride,
$statusCases
);
} catch (\Throwable $e) {
CLI::error("[{$tpaKey}] " . $e->getMessage());
$summary[$tpaKey] = [
'ok' => false,
'error' => $e->getMessage(),
];
}
}
CLI::newLine();
CLI::write(str_repeat('=', 64), 'cyan');
CLI::write('E2E SUMMARY', 'cyan');
CLI::write(str_repeat('=', 64), 'cyan');
foreach ($summary as $tpaKey => $row) {
$ok = !empty($row['ok']);
$line = sprintf(
'%-12s %s file_id=%s job1=%s job2=%s%s',
strtoupper($tpaKey),
$ok ? 'OK' : 'FAIL',
$row['file_id'] ?? '-',
$row['job1'] ?? '-',
$row['job2'] ?? '-',
!empty($row['error']) ? ' | ' . $row['error'] : ''
);
CLI::write($line, $ok ? 'green' : 'red');
if (!empty($row['status_cases'])) {
foreach ($row['status_cases'] as $case) {
$caseOk = !empty($case['ok']);
CLI::write(sprintf(
' %-10s dump=%s expected=%s got=%s %s',
$case['label'],
$case['dump_status'] === '' ? '(empty)' : $case['dump_status'],
$case['expected_id'],
$case['actual_id'] ?? 'null',
$caseOk ? 'PASS' : 'FAIL'
), $caseOk ? 'green' : 'red');
}
}
}
if (!$apply) {
CLI::newLine();
CLI::write('Dry-run only (Excel + claim_dump_files created). Re-run with --apply to execute Job1+Job2.', 'yellow');
}
}
private function resolveSelectedTpas(array $configs): array
{
$tpaIdOpt = (int) $this->resolveOptionValue('tpa-id', '0');
if ($tpaIdOpt > 0) {
foreach ($configs as $key => $cfg) {
if ((int) env($cfg['env']) === $tpaIdOpt) {
return [$key];
}
}
CLI::error("No TPA config matches --tpa-id={$tpaIdOpt}");
return [];
}
$tpaOpt = strtolower((string) ($this->resolveOptionValue('tpa', '') ?: 'all'));
if ($tpaOpt === '' || $tpaOpt === 'all') {
return array_keys($configs);
}
if (!isset($configs[$tpaOpt])) {
CLI::error("Unknown --tpa={$tpaOpt}. Use: " . implode('|', array_keys($configs)) . '|all');
return [];
}
return [$tpaOpt];
}
private function runForTpa(
$db,
string $tpaKey,
array $cfg,
bool $apply,
bool $truncate,
string $statusOverride,
bool $statusCases = false
): array {
$tpaId = (int) env($cfg['env']);
CLI::write("{$cfg['env']} = {$tpaId}", 'cyan');
CLI::write("table={$cfg['table']}, sheet=" . ($cfg['sheet'] ?? 'active'), 'cyan');
if (!$this->tableExists($db, $cfg['table'])) {
throw new \RuntimeException("Table {$cfg['table']} does not exist");
}
$context = $this->resolveSeedContext($db, $tpaId, $tpaKey, $cfg, $statusOverride);
CLI::write('Seed ticket: ' . json_encode([
'id' => $context['ticket']['id'] ?? null,
'client_id' => $context['client_id'],
'client_policy_id' => $context['client_policy_id'],
'policy_no' => $context['policy_no'],
'emp_code' => $context['emp_code'],
'tpa_no' => $context['tpa_no'],
'claim_amount' => $context['claim_amount'],
'doa' => $context['doa'],
'status_value' => $context['status_value'],
]));
$service = TpaClaimsImportFactory::make($tpaId);
$headers = $this->extractExcelHeaders($service);
if ($headers === []) {
throw new \RuntimeException('Could not read Excel headers from service mapping');
}
$scenarios = $statusCases
? $this->buildStatusCaseScenarios($cfg)
: [['label' => 'default', 'status_value' => $context['status_value'], 'claim_amount' => $context['claim_amount'], 'expected_id' => null]];
$excelRows = [];
foreach ($scenarios as $i => $scenario) {
$rowContext = $context;
$rowContext['status_value'] = $scenario['status_value'];
$rowContext['claim_amount'] = $scenario['claim_amount'];
$excelRows[] = $this->buildExcelRow($headers, $cfg, $rowContext);
CLI::write(sprintf(
'Row %d [%s]: status=%s amount=%s expected_id=%s',
$i + 1,
$scenario['label'],
$scenario['status_value'] === '' ? '(empty)' : $scenario['status_value'],
$scenario['claim_amount'],
$scenario['expected_id'] ?? 'n/a'
), 'cyan');
}
$fileName = $statusCases
? "{$tpaKey}_status_cases_" . date('Ymd_His') . '.xlsx'
: "{$tpaKey}_e2e_" . date('Ymd_His') . '.xlsx';
$uploadDir = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'claim_dump_excel' . DIRECTORY_SEPARATOR;
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$filePath = $uploadDir . $fileName;
$this->writeExcel($filePath, $headers, $excelRows, $cfg['sheet'] ?? null);
CLI::write("Excel written: {$filePath} (" . count($excelRows) . ' data rows)', 'green');
$fileId = $this->createClaimDumpFile($db, [
'tpa_id' => $tpaId,
'client_id' => $context['client_id'],
'client_policy_id' => $context['client_policy_id'],
'file_name' => $fileName,
'status' => 'inprogress',
'created_by' => 1,
'created_at' => date('Y-m-d H:i:s'),
'is_active' => 1,
]);
CLI::write("claim_dump_files.id = {$fileId}", 'green');
$result = [
'ok' => true,
'file_id' => $fileId,
'file_name' => $fileName,
'job1' => 'skipped',
'job2' => 'skipped',
];
if (!$apply) {
$applyHint = $statusCases
? "php spark tpa:e2e-pipeline --tpa={$tpaKey} --status-cases --apply"
: "php spark tpa:e2e-pipeline --tpa={$tpaKey} --apply";
CLI::write("Dry-run ready. Apply with: {$applyHint}", 'yellow');
return $result;
}
// Clear claim_dump_ref_id so link/update path can re-bind cleanly.
if (!$statusCases && !empty($context['ticket']['id']) && !empty($context['ticket']['claim_dump_ref_id'])) {
$db->table('ticket_master')
->where('id', $context['ticket']['id'])
->update(['claim_dump_ref_id' => null]);
CLI::write('Cleared seed ticket.claim_dump_ref_id for retest.', 'yellow');
}
CLI::write('Running Job 1 (Excel → dump)...', 'light_red');
$job1 = $service->runTpaClaimDumpInsert($filePath, $fileId);
CLI::write('Job1 result: ' . json_encode($job1));
$result['job1'] = !empty($job1['status']) ? 'ok' : 'fail';
if (empty($job1['status'])) {
$db->table('claim_dump_files')->where('id', $fileId)->update([
'status' => 'failed',
'reason' => json_encode(['error_data' => $job1['message'] ?? 'Job1 failed']),
]);
$result['ok'] = false;
$result['error'] = $job1['message'] ?? 'Job1 failed';
return $result;
}
$dumpCount = $db->table($cfg['table'])->where('file_id', $fileId)->where('is_active', 1)->countAllResults();
CLI::write("Dump rows after Job1: {$dumpCount}", 'cyan');
CLI::write('Running Job 2 (dump → ticket_master)...', 'light_red');
$job2 = $service->runTicketMasterInsert(['file_id' => $fileId]);
CLI::write('Job2 result: ' . json_encode($job2));
$result['job2'] = !empty($job2['status']) ? 'ok' : 'fail';
if (!empty($job2['status'])) {
$db->table('claim_dump_files')->where('id', $fileId)->update([
'status' => 'success',
'reason' => null,
]);
} else {
$db->table('claim_dump_files')->where('id', $fileId)->update([
'status' => 'failed',
'reason' => json_encode(['error_data' => $job2['message'] ?? 'Job2 failed']),
]);
$result['ok'] = false;
$result['error'] = $job2['message'] ?? 'Job2 failed';
return $result;
}
$statusCol = $cfg['status_db_column'] ?? null;
$selectCols = 'id, ticket_id, master_reject_reason, is_active';
if ($statusCol) {
$selectCols .= ', ' . $statusCol;
}
$afterDump = $db->table($cfg['table'])
->select($selectCols)
->where('file_id', $fileId)
->orderBy('id', 'ASC')
->get()->getResultArray();
CLI::write('Dump AFTER Job2: ' . json_encode($afterDump, JSON_PRETTY_PRINT));
$linked = 0;
foreach ($afterDump as $drow) {
if (!empty($drow['ticket_id'])) {
$linked++;
}
}
$result['linked'] = $linked;
if ($linked === 0 && empty($afterDump[0]['master_reject_reason'])) {
$result['ok'] = false;
$result['error'] = 'Job2 reported success but dump ticket_id is still null';
CLI::error($result['error']);
return $result;
}
if ($statusCases) {
$caseResults = $this->assertStatusCases($db, $cfg, $afterDump, $scenarios);
$result['status_cases'] = $caseResults;
foreach ($caseResults as $case) {
if (empty($case['ok'])) {
$result['ok'] = false;
$result['error'] = 'Status case assertion failed for: ' . $case['label'];
}
}
}
if ($truncate) {
CLI::write('Running soft truncate...', 'light_red');
$trunc = $service->softTruncateClaimDump($fileId);
CLI::write('Truncate: ' . json_encode($trunc));
$result['truncated'] = !empty($trunc['status']);
if (empty($trunc['status'])) {
$result['ok'] = false;
$result['error'] = $trunc['message'] ?? 'Truncate failed';
}
}
return $result;
}
/**
* Three rows: empty status 61, unmapped 61, mapped mapped_status_id.
* Unique claim amounts so Job 2 creates/links distinct tickets.
*/
private function buildStatusCaseScenarios(array $cfg): array
{
$suffix = (string) (time() % 100000);
$mappedStatus = (string) ($cfg['mapped_status'] ?? 'Paid');
$mappedId = (int) ($cfg['mapped_status_id'] ?? 11);
return [
[
'label' => 'empty',
'status_value' => '',
'claim_amount' => '9' . $suffix . '.11',
'expected_id' => 61,
],
[
'label' => 'unmapped',
'status_value' => 'ZZZ_UNMAPPED_STATUS',
'claim_amount' => '9' . $suffix . '.22',
'expected_id' => 61,
],
[
'label' => 'mapped',
'status_value' => $mappedStatus,
'claim_amount' => '9' . $suffix . '.33',
'expected_id' => $mappedId,
],
];
}
/**
* Match dump rows to scenarios by amount (and status text) and assert ticket claim_status_id.
*/
private function assertStatusCases($db, array $cfg, array $afterDump, array $scenarios): array
{
$statusCol = $cfg['status_db_column'] ?? null;
$results = [];
// Resolve amount column from excel_fields source key claim_amount → need dump amount column.
// Prefer joining via ticket_id and comparing expected_id on ticket_master.
foreach ($scenarios as $scenario) {
$match = null;
foreach ($afterDump as $drow) {
$dumpStatus = $statusCol !== null ? trim((string) ($drow[$statusCol] ?? '')) : '';
$expectedStatus = trim((string) $scenario['status_value']);
if ($dumpStatus === $expectedStatus) {
$match = $drow;
break;
}
}
$actualId = null;
$ticketId = $match['ticket_id'] ?? null;
if (!empty($ticketId)) {
$ticket = $db->table('ticket_master')
->select('id, claim_status_id')
->where('id', $ticketId)
->get()->getRowArray();
$actualId = isset($ticket['claim_status_id']) ? (int) $ticket['claim_status_id'] : null;
}
$ok = $match !== null
&& !empty($ticketId)
&& $actualId !== null
&& $actualId === (int) $scenario['expected_id'];
$results[] = [
'label' => $scenario['label'],
'dump_status' => $scenario['status_value'],
'expected_id' => (int) $scenario['expected_id'],
'actual_id' => $actualId,
'ticket_id' => $ticketId,
'dump_id' => $match['id'] ?? null,
'reject' => $match['master_reject_reason'] ?? null,
'ok' => $ok,
];
CLI::write(sprintf(
'ASSERT [%s] status=%s expected=%d actual=%s ticket_id=%s dump_id=%s %s%s',
$scenario['label'],
$scenario['status_value'] === '' ? '(empty)' : $scenario['status_value'],
(int) $scenario['expected_id'],
$actualId === null ? 'null' : (string) $actualId,
$ticketId ?? 'null',
$match['id'] ?? 'null',
$ok ? 'PASS' : 'FAIL',
!empty($match['master_reject_reason']) ? ' reject=' . $match['master_reject_reason'] : ''
), $ok ? 'green' : 'red');
}
return $results;
}
private function resolveSeedContext($db, int $tpaId, string $tpaKey, array $cfg, string $statusOverride): array
{
$ticket = $db->table('ticket_master')
->select('id, client_id, client_policy_id, emp_code, tpa_no, claim_amount, doa, claim_status_id, claim_dump_ref_id, tpa_id')
->where('tpa_id', $tpaId)
->where('is_active', 1)
->where('emp_code IS NOT NULL', null, false)
->where('tpa_no IS NOT NULL', null, false)
->where('claim_amount IS NOT NULL', null, false)
->where('doa IS NOT NULL', null, false)
->orderBy('id', 'DESC')
->get()->getRowArray();
if (empty($ticket)) {
CLI::write("[{$tpaKey}] No ticket for this tpa_id; falling back to any ticket with identity keys.", 'yellow');
$ticket = $db->table('ticket_master')
->select('id, client_id, client_policy_id, emp_code, tpa_no, claim_amount, doa, claim_status_id, claim_dump_ref_id, tpa_id')
->where('is_active', 1)
->where('client_policy_id IS NOT NULL', null, false)
->where('emp_code IS NOT NULL', null, false)
->where('tpa_no IS NOT NULL', null, false)
->where('claim_amount IS NOT NULL', null, false)
->where('doa IS NOT NULL', null, false)
->orderBy('id', 'DESC')
->get()->getRowArray();
}
if (empty($ticket)) {
throw new \RuntimeException('No suitable ticket_master row found to seed Excel');
}
$policy = $db->table('client_policy')
->select('id, client_id, policy_no')
->where('id', $ticket['client_policy_id'])
->get()->getRowArray();
if (empty($policy) || trim((string) ($policy['policy_no'] ?? '')) === '') {
throw new \RuntimeException('client_policy / policy_no missing for seed ticket');
}
$statusValue = $statusOverride !== '' ? $statusOverride : $cfg['default_status'];
return [
'ticket' => $ticket,
'client_id' => (int) ($ticket['client_id'] ?? $policy['client_id']),
'client_policy_id' => (int) $ticket['client_policy_id'],
'policy_no' => trim((string) $policy['policy_no']),
'emp_code' => (string) $ticket['emp_code'],
'tpa_no' => (string) $ticket['tpa_no'],
'claim_amount' => (string) $ticket['claim_amount'],
'doa' => (string) $ticket['doa'],
'relation' => 'SELF',
'status_value' => $statusValue,
];
}
private function extractExcelHeaders(object $service): array
{
$ref = new ReflectionClass($service);
if (!$ref->hasProperty('mapping')) {
return [];
}
$prop = $ref->getProperty('mapping');
$prop->setAccessible(true);
$mapping = $prop->getValue($service);
if (!is_array($mapping)) {
return [];
}
$headers = [];
foreach ($mapping as $map) {
$excel = $map['excel_column'] ?? null;
if (is_array($excel)) {
$name = trim((string) ($excel['col_name'] ?? ''));
} else {
$name = trim((string) $excel);
}
if ($name !== '') {
$headers[] = $name;
}
}
return $headers;
}
private function buildExcelRow(array $headers, array $cfg, array $context): array
{
$row = array_fill(0, count($headers), '');
$headerIndex = array_flip($headers);
foreach ($cfg['excel_fields'] as $excelHeader => $sourceKey) {
if (!isset($headerIndex[$excelHeader])) {
CLI::write("Warning: Excel header '{$excelHeader}' not in mapping; skipped.", 'yellow');
continue;
}
$row[$headerIndex[$excelHeader]] = $context[$sourceKey] ?? '';
}
// Ensure status header is filled even if excel_fields key differs.
if (isset($headerIndex[$cfg['status_header']])) {
$row[$headerIndex[$cfg['status_header']]] = $context['status_value'];
}
return $row;
}
private function writeExcel(string $filePath, array $headers, array $rows, ?string $sheetName): void
{
// Backward compatible: single row flat array becomes one data row.
if ($rows !== [] && !is_array($rows[0] ?? null)) {
$rows = [$rows];
}
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
if ($sheetName) {
$sheet->setTitle($sheetName);
}
foreach ($headers as $i => $header) {
$sheet->setCellValue([$i + 1, 1], $header);
}
foreach ($rows as $r => $rowValues) {
foreach ($headers as $i => $header) {
$sheet->setCellValue([$i + 1, $r + 2], $rowValues[$i] ?? '');
}
}
$writer = new Xlsx($spreadsheet);
$writer->save($filePath);
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
}
private function createClaimDumpFile($db, array $data): int
{
$cols = array_column($db->query('SHOW COLUMNS FROM claim_dump_files')->getResultArray(), 'Field');
$data = array_intersect_key($data, array_flip($cols));
$db->table('claim_dump_files')->insert($data);
$id = (int) $db->insertID();
if ($id <= 0) {
throw new \RuntimeException('Failed to insert claim_dump_files');
}
return $id;
}
private function tableExists($db, string $table): bool
{
return !empty($db->query('SHOW TABLES LIKE ' . $db->escape($table))->getResultArray());
}
private function hasFlag(string $name): bool
{
if (CLI::getOption($name) !== null) {
return true;
}
return in_array('--' . $name, $_SERVER['argv'] ?? [], true);
}
private function resolveOptionValue(string $name, string $default): string
{
$opt = CLI::getOption($name);
if (is_string($opt) && $opt !== '') {
return $opt;
}
$argv = $_SERVER['argv'] ?? [];
foreach ($argv as $i => $arg) {
if (preg_match('/^--' . preg_quote($name, '/') . '=(.+)$/', (string) $arg, $m)) {
return trim($m[1]);
}
if ($arg === '--' . $name && isset($argv[$i + 1]) && strpos((string) $argv[$i + 1], '--') !== 0) {
return (string) $argv[$i + 1];
}
}
return $default;
}
}

View File

@ -0,0 +1,551 @@
<?php
namespace App\Commands;
use App\Libraries\TpaClaimsImportFactory;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Dry-run / apply existing-ticket status+link update logic for any TPA.
*
* Usage:
* php spark tpa:test-status
* php spark tpa:test-status --tpa=all
* php spark tpa:test-status --tpa=abhi --scenario
* php spark tpa:test-status --tpa=fhpl --scenario --apply
* php spark tpa:test-status --tpa=icici 69 --apply
*/
class TestTpaStatusUpdate extends BaseCommand
{
protected $group = 'TPA';
protected $name = 'tpa:test-status';
protected $description = 'Dry-run / apply existing ticket status+link updates for TPA claim dumps';
protected $usage = 'tpa:test-status [file_id] [--tpa=NAME|all] [--scenario] [--apply] [--new-status=STATUS]';
protected $arguments = [
'file_id' => 'Optional claim_dump_files.id to process',
];
protected $options = [
'--tpa' => 'TPA key: icici|abhi|mediassist|fhpl|rcare|vidal|all (default: all for --scenario)',
'--scenario' => 'Seed one pending dump row matching an existing ticket',
'--apply' => 'Actually run runTicketMasterInsert (default is map-only dry-run)',
'--new-status' => 'Dump status string for scenario (defaults per TPA)',
];
/**
* Per-TPA dump column mapping used to seed scenario rows.
*/
private function tpaConfigs(): array
{
return [
'icici' => [
'env' => 'ICICI_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_icici',
'status_column' => 'updated_status',
'default_status'=> 'REJECTED',
'same_status' => 'PAID',
'dump_to_ticket'=> [
'employee_member_id' => 'emp_code',
'uhid' => 'tpa_no',
'claimed_amount' => 'claim_amount',
'doa' => 'doa',
],
'extra_dump' => [
'relation' => 'SELF',
],
],
'abhi' => [
'env' => 'ABHI_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_abhi',
'status_column' => 'claim_status',
'default_status'=> 'Rejected',
'same_status' => 'Settled',
'dump_to_ticket'=> [
'member_code' => 'emp_code',
'healthcard_id' => 'tpa_no',
'claimed_amount' => 'claim_amount',
'doa' => 'doa',
],
'extra_dump' => [
'relation' => 'SELF',
],
],
'mediassist' => [
'env' => 'MEDI_ASSIST_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_medi_assist',
'status_column' => 'claim_status',
'default_status'=> 'Rejected',
'same_status' => 'Settled',
'dump_to_ticket'=> [
'pribenef_employee_code' => 'emp_code',
'event_id' => 'tpa_no',
'claim_amount' => 'claim_amount',
'date_of_admission' => 'doa',
],
'extra_dump' => [
'benef_relation' => 'SELF',
],
],
'fhpl' => [
'env' => 'FHPL_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_fhpl',
'status_column' => 'current_claim_status',
'default_status'=> 'Rejected',
'same_status' => 'Settled',
'dump_to_ticket'=> [
'employee_id' => 'emp_code',
'uhid_no' => 'tpa_no',
'claim_amount' => 'claim_amount',
'admission_date' => 'doa',
],
'extra_dump' => [
'relationship' => 'SELF',
],
],
'rcare' => [
'env' => 'R_CARE_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_reliance',
'status_column' => 'final_status',
'default_status'=> 'Rejected',
'same_status' => 'Settled',
'dump_to_ticket'=> [
'employee_member_id' => 'emp_code',
'uhid' => 'tpa_no',
'claimed_amount' => 'claim_amount',
'doa_opd_treatment_from' => 'doa',
],
'extra_dump' => [
'relation' => 'SELF',
],
],
'vidal' => [
'env' => 'VIDAL_PRIMARY_KEY_CONSTANT',
'table' => 'claims_dump_vidal',
'status_column' => 'claim_status',
'default_status'=> 'Rejected',
'same_status' => 'Settled',
'dump_to_ticket'=> [
'employee_number' => 'emp_code',
'primary_policy_holder_card_id' => 'tpa_no',
'claim_amount' => 'claim_amount',
'date_of_admission' => 'doa',
],
'extra_dump' => [
'relation' => 'SELF',
],
],
];
}
public function run(array $params)
{
helper('utility_helper');
$db = db_connect();
$apply = $this->hasFlag('apply');
$scenario = $this->hasFlag('scenario');
$tpaOpt = strtolower((string) ($this->resolveOptionValue('tpa', '') ?: (CLI::getOption('tpa') ?? '')));
$fileId = $this->resolveFileId($params);
$newStatusOverride = $this->resolveOptionValue('new-status', '');
$configs = $this->tpaConfigs();
// Resolve which TPAs to run
if ($tpaOpt === '' || $tpaOpt === 'all') {
$selected = array_keys($configs);
if (!$scenario && $fileId <= 0) {
$this->printUsage($configs);
return;
}
} elseif (isset($configs[$tpaOpt])) {
$selected = [$tpaOpt];
} else {
CLI::error("Unknown --tpa={$tpaOpt}. Use: " . implode('|', array_keys($configs)) . '|all');
return;
}
foreach ($selected as $tpaKey) {
CLI::newLine();
CLI::write(str_repeat('=', 60), 'yellow');
CLI::write('TPA: ' . strtoupper($tpaKey), 'yellow');
CLI::write(str_repeat('=', 60), 'yellow');
try {
$this->runForTpa(
$db,
$tpaKey,
$configs[$tpaKey],
$scenario,
$apply,
$fileId,
$newStatusOverride
);
} catch (\Throwable $e) {
CLI::error("[{$tpaKey}] " . $e->getMessage());
}
}
}
private function runForTpa(
$db,
string $tpaKey,
array $cfg,
bool $scenario,
bool $apply,
int $fileId,
string $newStatusOverride
): void {
$tpaId = (int) env($cfg['env']);
CLI::write("{$cfg['env']} = {$tpaId}", 'cyan');
CLI::write("table={$cfg['table']}, status_column={$cfg['status_column']}", 'cyan');
if (!$this->tableExists($db, $cfg['table'])) {
CLI::error("Table {$cfg['table']} does not exist. Skipping.");
return;
}
$newStatus = $newStatusOverride !== '' ? $newStatusOverride : $cfg['default_status'];
$currentFileId = $fileId;
if ($scenario) {
$currentFileId = $this->seedScenario($db, $tpaKey, $cfg, $tpaId, $newStatus);
if ($currentFileId <= 0) {
return;
}
}
if ($currentFileId <= 0) {
$this->listRecentFiles($db, $tpaId, $tpaKey);
return;
}
$file = $db->table('claim_dump_files')->where('id', $currentFileId)->get()->getRowArray();
if (empty($file)) {
CLI::error("claim_dump_files id={$currentFileId} not found");
return;
}
CLI::write('File: ' . json_encode([
'id' => $file['id'],
'tpa_id' => $file['tpa_id'],
'status' => $file['status'],
'file_name' => $file['file_name'],
'client_policy_id' => $file['client_policy_id'],
]));
if ((int) $file['tpa_id'] !== $tpaId) {
CLI::error("File tpa_id={$file['tpa_id']} does not match {$tpaKey} ({$tpaId}). Skipping.");
return;
}
$pending = $db->table($cfg['table'])
->where('file_id', $currentFileId)
->where('is_active', 1)
->where('ticket_id IS NULL', null, false)
->where('master_reject_reason IS NULL', null, false)
->countAllResults();
CLI::write("Pending dump rows: {$pending}");
$service = TpaClaimsImportFactory::make($tpaId);
if ($apply) {
CLI::write('APPLY mode...', 'light_red');
$beforeDump = $db->table($cfg['table'])
->select($this->dumpSelectColumns($cfg))
->where('file_id', $currentFileId)
->where('is_active', 1)
->get()->getResultArray();
CLI::write('Dump BEFORE: ' . json_encode($beforeDump, JSON_PRETTY_PRINT));
$result = $service->runTicketMasterInsert(['file_id' => $currentFileId]);
CLI::write('Result: ' . json_encode($result, JSON_PRETTY_PRINT));
// Mirror TicketServiceController Job 2 success/fail file status update.
if (!empty($result['status'])) {
$db->table('claim_dump_files')->where('id', $currentFileId)->update([
'status' => 'success',
'reason' => null,
]);
CLI::write("claim_dump_files.status => success", 'green');
} else {
$db->table('claim_dump_files')->where('id', $currentFileId)->update([
'status' => 'failed',
'reason' => json_encode([
'error_summary' => [5 => 1],
'error_data' => $result['message'] ?? 'Job 2 failed',
]),
]);
CLI::write("claim_dump_files.status => failed", 'red');
}
$afterDump = $db->table($cfg['table'])
->select($this->dumpSelectColumns($cfg))
->where('file_id', $currentFileId)
->where('is_active', 1)
->get()->getResultArray();
CLI::write('Dump AFTER: ' . json_encode($afterDump, JSON_PRETTY_PRINT));
$ticketIds = array_values(array_filter(array_column($afterDump, 'ticket_id')));
if (!empty($ticketIds)) {
$tickets = $db->table('ticket_master')
->select('id, claim_status_id, claim_dump_ref_id, emp_code, tpa_no, claim_amount, doa')
->whereIn('id', $ticketIds)
->get()->getResultArray();
CLI::write('Tickets AFTER: ' . json_encode($tickets, JSON_PRETTY_PRINT));
}
return;
}
CLI::write('DRY-RUN mode (mapClaimMasterData only)...', 'green');
$ref = new \ReflectionClass($service);
$method = $ref->getMethod('mapClaimMasterData');
$method->setAccessible(true);
$mapped = $method->invoke($service, $currentFileId);
if (empty($mapped['status'])) {
CLI::write('mapClaimMasterData: ' . ($mapped['message'] ?? 'failed'));
if (!empty($mapped['already_processed'])) {
CLI::write('(already processed — dump rows were NOT deleted)', 'green');
}
return;
}
$inserts = $mapped['mapped_array'] ?? [];
$rejects = $mapped['rejected_reason_array'] ?? [];
$updates = $mapped['status_update_array'] ?? [];
CLI::write('inserts=' . count($inserts) . ' updates=' . count($updates) . ' rejects=' . count($rejects));
CLI::write('status_update_array: ' . (empty($updates) ? '(none)' : json_encode($updates, JSON_PRETTY_PRINT)));
CLI::write('rejected_reason_array: ' . json_encode(array_slice($rejects, 0, 10), JSON_PRETTY_PRINT));
if (!empty($updates)) {
$ids = array_column($updates, 'id');
$before = $db->table('ticket_master')
->select('id, emp_code, tpa_no, claim_amount, doa, claim_status_id, claim_dump_ref_id')
->whereIn('id', $ids)
->get()->getResultArray();
CLI::write('Tickets BEFORE: ' . json_encode($before, JSON_PRETTY_PRINT));
}
CLI::write("To apply: php spark tpa:test-status --tpa={$tpaKey} {$currentFileId} --apply", 'green');
}
private function seedScenario($db, string $tpaKey, array $cfg, int $tpaId, string $newStatus): int
{
CLI::write("Seeding scenario for {$tpaKey} (new_status={$newStatus})...", 'cyan');
$ticket = $db->table('ticket_master')
->select('id, client_id, client_policy_id, emp_code, tpa_no, claim_amount, doa, claim_status_id, claim_dump_ref_id, tpa_id')
->where('tpa_id', $tpaId)
->where('is_active', 1)
->where('emp_code IS NOT NULL', null, false)
->where('tpa_no IS NOT NULL', null, false)
->where('claim_amount IS NOT NULL', null, false)
->where('doa IS NOT NULL', null, false)
->orderBy('id', 'DESC')
->get()->getRowArray();
if (empty($ticket)) {
// Fall back: any ticket with identity keys (still allows match by doa/emp/amount/tpa_no)
$ticket = $db->table('ticket_master')
->select('id, client_id, client_policy_id, emp_code, tpa_no, claim_amount, doa, claim_status_id, claim_dump_ref_id, tpa_id')
->where('is_active', 1)
->where('emp_code IS NOT NULL', null, false)
->where('tpa_no IS NOT NULL', null, false)
->where('claim_amount IS NOT NULL', null, false)
->where('doa IS NOT NULL', null, false)
->orderBy('id', 'DESC')
->get()->getRowArray();
}
if (empty($ticket)) {
CLI::error("[{$tpaKey}] No suitable existing ticket found to seed.");
return 0;
}
CLI::write('Using ticket: ' . json_encode([
'id' => $ticket['id'],
'emp_code' => $ticket['emp_code'],
'tpa_no' => $ticket['tpa_no'],
'claim_amount' => $ticket['claim_amount'],
'doa' => $ticket['doa'],
'claim_status_id' => $ticket['claim_status_id'],
'claim_dump_ref_id' => $ticket['claim_dump_ref_id'],
'tpa_id' => $ticket['tpa_id'],
]));
if (!empty($ticket['claim_dump_ref_id'])) {
$db->table('ticket_master')->where('id', $ticket['id'])->update(['claim_dump_ref_id' => null]);
CLI::write('Cleared stale ticket.claim_dump_ref_id for retest.', 'yellow');
}
$uploadDir = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'claim_dump_excel' . DIRECTORY_SEPARATOR;
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$fileName = "{$tpaKey}_status_test_" . date('Ymd_His') . '.xlsx';
file_put_contents($uploadDir . $fileName, 'placeholder');
$fileInsert = [
'tpa_id' => $tpaId,
'client_id' => $ticket['client_id'],
'client_policy_id' => $ticket['client_policy_id'],
'file_name' => $fileName,
'status' => 'pending',
'created_by' => 1,
'created_at' => date('Y-m-d H:i:s'),
'is_active' => 1,
];
$fileCols = array_column($db->query('SHOW COLUMNS FROM claim_dump_files')->getResultArray(), 'Field');
$fileInsert = array_intersect_key($fileInsert, array_flip($fileCols));
$db->table('claim_dump_files')->insert($fileInsert);
$fileId = (int) $db->insertID();
if ($fileId <= 0) {
CLI::error("[{$tpaKey}] Failed to create claim_dump_files row.");
return 0;
}
$dumpInsert = [
'client_id' => $ticket['client_id'],
'client_policy_id' => $ticket['client_policy_id'],
'file_id' => $fileId,
'is_active' => 1,
'created_by' => 1,
'created_at' => date('Y-m-d H:i:s'),
'ticket_id' => null,
'master_reject_reason' => null,
$cfg['status_column'] => $newStatus,
];
foreach ($cfg['dump_to_ticket'] as $dumpCol => $ticketCol) {
$dumpInsert[$dumpCol] = $ticket[$ticketCol] ?? null;
}
foreach ($cfg['extra_dump'] as $col => $val) {
$dumpInsert[$col] = $val;
}
$dumpCols = array_column($db->query('SHOW COLUMNS FROM ' . $cfg['table'])->getResultArray(), 'Field');
$dumpInsert = array_intersect_key($dumpInsert, array_flip($dumpCols));
$db->table($cfg['table'])->insert($dumpInsert);
$dumpId = (int) $db->insertID();
CLI::write("Seeded file_id={$fileId}, dump_id={$dumpId}", 'green');
return $fileId;
}
private function dumpSelectColumns(array $cfg): string
{
$cols = ['id', 'ticket_id', 'master_reject_reason', 'file_id', $cfg['status_column']];
foreach (array_keys($cfg['dump_to_ticket']) as $col) {
$cols[] = $col;
}
return implode(', ', array_unique($cols));
}
private function listRecentFiles($db, int $tpaId, string $tpaKey): void
{
$recent = $db->table('claim_dump_files')
->select('id, tpa_id, client_id, client_policy_id, file_name, status, created_at')
->where('tpa_id', $tpaId)
->orderBy('id', 'DESC')
->limit(5)
->get()->getResultArray();
CLI::write("Recent {$tpaKey} claim_dump_files:");
if (empty($recent)) {
CLI::write('(none)');
} else {
foreach ($recent as $row) {
CLI::write(json_encode($row));
}
}
CLI::write("Seed: php spark tpa:test-status --tpa={$tpaKey} --scenario", 'green');
CLI::write("Apply: php spark tpa:test-status --tpa={$tpaKey} --scenario --apply", 'green');
}
private function printUsage(array $configs): void
{
CLI::write('Test existing-ticket status/link update for TPA claim dumps.', 'cyan');
CLI::newLine();
CLI::write('Dry-run all TPAs (seed scenario, no DB ticket update):');
CLI::write(' php spark tpa:test-status --tpa=all --scenario');
CLI::newLine();
CLI::write('Apply all TPAs (writes to DB):');
CLI::write(' php spark tpa:test-status --tpa=all --scenario --apply');
CLI::newLine();
CLI::write('Single TPA:');
foreach (array_keys($configs) as $key) {
CLI::write(" php spark tpa:test-status --tpa={$key} --scenario");
CLI::write(" php spark tpa:test-status --tpa={$key} --scenario --apply");
}
CLI::newLine();
CLI::write('Same-status (no status change, only null-link fill):');
CLI::write(' php spark tpa:test-status --tpa=abhi --scenario --new-status Settled');
}
private function tableExists($db, string $table): bool
{
return !empty($db->query("SHOW TABLES LIKE " . $db->escape($table))->getResultArray());
}
private function resolveFileId(array $params): int
{
if (!empty($params['file_id'])) {
return (int) $params['file_id'];
}
if (!empty($params[0]) && is_numeric($params[0])) {
return (int) $params[0];
}
$argv = $_SERVER['argv'] ?? [];
foreach ($argv as $i => $arg) {
if (preg_match('/^--file[_-]id=(.+)$/', (string) $arg, $m)) {
return (int) $m[1];
}
if (in_array($arg, ['--file_id', '--file-id'], true) && isset($argv[$i + 1])) {
return (int) $argv[$i + 1];
}
// Bare numeric arg (e.g. `tpa:test-status --tpa=icici 70 --apply`)
if (is_numeric($arg) && (int) $arg > 0 && $i > 0 && strpos((string) $argv[$i - 1], 'spark') === false) {
$prev = (string) ($argv[$i - 1] ?? '');
// Skip values belonging to options like --tpa=... or --new-status STATUS
if ($prev === '--tpa' || $prev === '--new-status' || $prev === '--file_id' || $prev === '--file-id') {
continue;
}
return (int) $arg;
}
}
return (int) (CLI::getOption('file_id') ?? CLI::getOption('file-id') ?? 0);
}
private function hasFlag(string $name): bool
{
if (CLI::getOption($name) !== null) {
return true;
}
return in_array('--' . $name, $_SERVER['argv'] ?? [], true);
}
private function resolveOptionValue(string $name, string $default): string
{
$opt = CLI::getOption($name);
if (is_string($opt) && $opt !== '') {
return $opt;
}
$argv = $_SERVER['argv'] ?? [];
foreach ($argv as $i => $arg) {
if (preg_match('/^--' . preg_quote($name, '/') . '=(.+)$/', (string) $arg, $m)) {
return trim($m[1]);
}
if ($arg === '--' . $name && isset($argv[$i + 1]) && strpos((string) $argv[$i + 1], '--') !== 0) {
return (string) $argv[$i + 1];
}
}
return $default;
}
}

15
app/Common.php Executable file
View File

@ -0,0 +1,15 @@
<?php
/**
* The goal of this file is to allow developers a location
* where they can overwrite core procedural functions and
* replace them with their own. This file is loaded during
* the bootstrap process and is called during the framework's
* execution.
*
* This can be looked at as a `master helper` file that is
* loaded early on, and may also contain additional functions
* that you'd like to use throughout your entire application
*
* @see: https://codeigniter.com/user_guide/extending/common.html
*/

339
app/Config/Acl.php Normal file
View File

@ -0,0 +1,339 @@
<?php
namespace Config;
class Acl
{
public array $rules = [
// ===================== PUBLIC / AUTH =====================
'#^/login#' => ['public' => true],
'#^/downloadCdSplitUpFile#' => ['public' => true],
'#^/logout#' => ['public' => true],
'#^/auth#' => ['public' => true],
'#^/oauth2callback#' => ['public' => true],
'#^/loginPos#' => ['public' => true],
'#^/getVerifyPosMobileNo#' => ['public' => true],
'#^/getVerifiedPosUserData#' => ['public' => true],
'#^/chat#' => ['public' => true],
'#^/widget#' => ['public' => true],
'#^/chatbottest#' => ['public' => true],
'#^/chatbot#' => ['public' => true],
'#^/getSSORedirectUrl#' => ['public' => true],
'#^/getEmployeePolicy#' => ['public' => true],
'#^/downloadFileTableFile#' => ['public' => true],
'#^/storage/file-download#' => ['public' => true],
'#^/fileDownload#' => ['public' => true],
'#^/downloadClaimFile#' => ['public' => true],
'#^/swagger#' => ['roles' => [ADMIN_ROLE_ID]],
'#^/getEmployeeActiveOrInactivePolicy#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/test/chartbrewDashboardDemo#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/bulkEcardDownloadAsZip#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/sheet#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/sendextraparam#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/test/testingquerys#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/test/viewrfq#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/fedeploy#' => ['roles' => [ADMIN_ROLE_ID]],
'#^/storage/files#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/visitOffBoardCheck#' => ['roles' => [ADMIN_ROLE_ID]],
'#^/logs#' => ['roles' => [ADMIN_ROLE_ID]],
'#^/util/log_list#' => ['roles' => [ADMIN_ROLE_ID]],
'#^/util/view_log#' => ['roles' => [ADMIN_ROLE_ID]],
'#^/util/download_log#' => ['roles' => [ADMIN_ROLE_ID]],
'#^/metaDashboardDemo#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/metaTpaDashboardDemo#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/sales/dashboard#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
'#^/sales#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
'#^/expense#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
'#^/sendDataToTPA#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID]],
'#^/swagger#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID]],
'#^/viewClaimFile#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID]],
'#^/employee/tpaReportsDashboard#' => ['roles' => [ACCOUNT_MANAGER_ROLE_ID, ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/policy_tranction/sendInstallmentRemainderMail#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
// ===================== PUBLIC DOWNLOADS / FORMS =====================
'#^/download-#' => ['public' => true],
'#^/claim-form-download#' => ['public' => true],
'#^/claims-feedback-form#' => ['public' => true],
'#^/autobookstackLogin#' => ['public' => true],
// ===================== DASHBOARD =====================
'#^/dashboard#' => [
'roles' => [ ADMIN_ROLE_ID,HEAD_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => []
],
// ===================== VIEW INCEPTION PAGE =====================
'#^/hrFileList#' => [
'roles' => [ ADMIN_ROLE_ID,HEAD_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => []
],
'#^/hrFileDownload#' => [
'roles' => [ ADMIN_ROLE_ID,HEAD_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => []
],
'#^/sendPushNotification#' => [
'roles' => [ ADMIN_ROLE_ID,HEAD_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => []
],
'#^/sendEmail#' => [
'roles' => [ ADMIN_ROLE_ID,HEAD_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => []
],
'#^/getPolicyLevelEmployeeSummaryData#' => [
'roles' => [ ADMIN_ROLE_ID,HEAD_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => []
],
'#^/cdTransactionData#' => [
'roles' => [ ADMIN_ROLE_ID,HEAD_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => []
],
'#^/claimsSearch#' => [
'roles' => [ ADMIN_ROLE_ID,HEAD_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/getBackToEnrolledDetails#' => [
'roles' => [ ADMIN_ROLE_ID,HEAD_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => []
],
// ===================== USER MANAGEMENT =====================
'#^/user#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID],
'teams' => []
],
// ===================== CLIENT =====================
'#^/client#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => []
],
// ===================== EMPLOYEE / ENROLLMENT =====================
'#^/employee#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [ENROLLMENT_TEAM_ID]
],
// ===================== MASTERS =====================
'#^/master#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => []
],
'#^/util#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID],
'teams' => []
],
// ===================== DIGIT MOTOR =====================
'#^/digit-motor#' => [
'roles' => [ HEAD_ROLE_ID, ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, STAFF_ROLE_ID],
'teams' => [FINANCE_TEAM_ID, POS_TEAM_ID, BUSINESS_TEAM_ID, MANAGEMENT_TEAM_ID]
],
// ===================== POLICY TRANSACTION / BDS =====================
'#^/policy_tranction#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID],
'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID]
],
'#^/dmsSearch#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID]
],
'#^/downloadGdriveFile#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID]
],
'#^/driveListFiles#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID]
],
'#^/bds_upload#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID]
],
// ===================== REPORTS =====================
'#^/bdsReport#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID]
],
// ===================== PAYOUT / COMMISSION =====================
'#^/payout#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID]
],
'#^/commission#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID]
],
'#^/importRules#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [FINANCE_TEAM_ID,POS_TEAM_ID]
],
// ===================== CLAIMS / TICKETS =====================
'#^/ticket#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/ticketSave#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/ticketList#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/ticketConversationSave#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/ticketConversationList#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/ticketType#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/ticketHistoryList#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/ticketAutoFetchDetails#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/initiateClaim#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/non-eb-claim#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/departments#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/get_ticket_data#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/claimView#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
'#^/claim_mis#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [CLAIMS_TEAM_ID]
],
// ===================== LEADS / SALES =====================
'#^/leads#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [BUSINESS_SUPPORT_TEAM_ID, SALES_TEAM_ID]
],
'#^/rfq#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
'teams' => [BUSINESS_SUPPORT_TEAM_ID, SALES_TEAM_ID]
],
'#^/sales#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID],
'teams' => [BUSINESS_SUPPORT_TEAM_ID, SALES_TEAM_ID]
],
// ===================== CMS / CONTENT =====================
'#^/add_image_index#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID],
'teams' => []
],
'#^/add_advertise_image#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID],
'teams' => []
],
'#^/remove_advertise_image#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID],
'teams' => []
],
'#^/getAdvertiseImage#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID],
'teams' => []
],
'#^/showAdvertiseImage#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID],
'teams' => []
],
'#^/frontend_content#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID],
'teams' => []
],
'#^/FAQ#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID],
'teams' => []
],
'#^/getFEContent#' => [
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID],
'teams' => []
],
// ===================== LOGS =====================
'#^/logs#' => [
'roles' => [ADMIN_ROLE_ID],
'teams' => []
],
// ===================== RATE LIMIT ADMIN =====================
'#^/security/rate-limits#' => [
'roles' => [ADMIN_ROLE_ID],
'teams' => []
],
// ===================== INTERNAL TEST =====================
'#^/test#' => [
'roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID],
'teams' => []
],
// ===================== API (JWT / SIGNED) =====================
'#^/api#' => ['public' => true],
'#^/employeeRest#' => ['public' => true],
'#^/clientApi#' => ['public' => true],
// ===================== WEBHOOKS / 3RD PARTY =====================
'#^/dispatchWebhookData#' => ['public' => true],
'#^/retrieveWebhookData#' => ['public' => true],
'#^/ecardRequest#' => ['public' => true],
'#^/getTPAID#' => ['public' => true],
'#^/fileDownload#' => ['public' => true],
'#^/generateAuthToken#' => ['public' => true],
'#^/createEnrollmentBatch#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID], 'teams' => [ENROLLMENT_TEAM_ID]],
'#^/getEnrollmentBatchStatus#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID], 'teams' => [ENROLLMENT_TEAM_ID]],
'#^/fetchUHIDDetails#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID], 'teams' => [ENROLLMENT_TEAM_ID]],
'#^/FhplGetBenefDetails#' => ['roles' => [ADMIN_ROLE_ID]],
'#^/EcardRequest#' => ['public' => true],
'#^/HospitalNetwork#' => ['public' => true],
'#^/VidalGetBenefDetails#' => ['public' => true],
'#^/ClaimDetail#' => ['public' => true],
'#^/SubmitClaim#' => ['roles' => [ADMIN_ROLE_ID,HEAD_ROLE_ID,ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID]],
'#^/IntimateClaim#' => ['roles' => [ADMIN_ROLE_ID,HEAD_ROLE_ID,ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID]],
'#^/IRSubmission#' => ['roles' => [ADMIN_ROLE_ID,HEAD_ROLE_ID,ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID]],
'#^/ClaimStatusUpdate#' => ['roles' => [ADMIN_ROLE_ID,HEAD_ROLE_ID,ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID]],
'#^/syncTpaClaimToNhance#' => ['roles' => [ADMIN_ROLE_ID,HEAD_ROLE_ID,ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID]],
'#^/HealthIndiaGetBenefDetails#' => ['roles' => [ADMIN_ROLE_ID,HEAD_ROLE_ID,ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID]],
'#^/testTracelog#' => ['roles' => [ADMIN_ROLE_ID]],
// ===================== CLI =====================
'#^/cli/#' => ['public' => true],
// ===================== DEFAULT DENY (ZERO TRUST) =====================
'#^/#' => [
'roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID],
'teams' => []
],
];
}

180
app/Config/App.php Executable file
View File

@ -0,0 +1,180 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class App extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Base Site URL
* --------------------------------------------------------------------------
*
* URL to your CodeIgniter root. Typically, this will be your base URL,
* WITH a trailing slash:
*
* http://example.com/
*/
public string $baseURL = 'http://localhost/nhance/';
/**
* Allowed Hostnames in the Site URL other than the hostname in the baseURL.
* If you want to accept multiple Hostnames, set this.
*
* E.g. When your site URL ($baseURL) is 'http://example.com/', and your site
* also accepts 'http://media.example.com/' and
* 'http://accounts.example.com/':
* ['media.example.com', 'accounts.example.com']
*
* @var list<string>
*/
public array $allowedHostnames = [];
/**
* --------------------------------------------------------------------------
* Index File
* --------------------------------------------------------------------------
*
* Typically this will be your index.php file, unless you've renamed it to
* something else. If you are using mod_rewrite to remove the page set this
* variable so that it is blank.
*/
public string $indexPage = 'index.php';
public $compressOutput = true;
/**
* --------------------------------------------------------------------------
* URI PROTOCOL
* --------------------------------------------------------------------------
*
* This item determines which server global should be used to retrieve the
* URI string. The default setting of 'REQUEST_URI' works for most servers.
* If your links do not seem to work, try one of the other delicious flavors:
*
* 'REQUEST_URI' Uses $_SERVER['REQUEST_URI']
* 'QUERY_STRING' Uses $_SERVER['QUERY_STRING']
* 'PATH_INFO' Uses $_SERVER['PATH_INFO']
*
* WARNING: If you set this to 'PATH_INFO', URIs will always be URL-decoded!
*/
public string $uriProtocol = 'REQUEST_URI';
/**
* --------------------------------------------------------------------------
* Default Locale
* --------------------------------------------------------------------------
*
* The Locale roughly represents the language and location that your visitor
* is viewing the site from. It affects the language strings and other
* strings (like currency markers, numbers, etc), that your program
* should run under for this request.
*/
public string $defaultLocale = 'en';
/**
* --------------------------------------------------------------------------
* Negotiate Locale
* --------------------------------------------------------------------------
*
* If true, the current Request object will automatically determine the
* language to use based on the value of the Accept-Language header.
*
* If false, no automatic detection will be performed.
*/
public bool $negotiateLocale = false;
/**
* --------------------------------------------------------------------------
* Supported Locales
* --------------------------------------------------------------------------
*
* If $negotiateLocale is true, this array lists the locales supported
* by the application in descending order of priority. If no match is
* found, the first locale will be used.
*
* IncomingRequest::setLocale() also uses this list.
*
* @var string[]
*/
public array $supportedLocales = ['en'];
/**
* --------------------------------------------------------------------------
* Application Timezone
* --------------------------------------------------------------------------
*
* The default timezone that will be used in your application to display
* dates with the date helper, and can be retrieved through app_timezone()
*
* @see https://www.php.net/manual/en/timezones.php for list of timezones supported by PHP.
*/
public string $appTimezone = 'UTC';
/**
* --------------------------------------------------------------------------
* Default Character Set
* --------------------------------------------------------------------------
*
* This determines which character set is used by default in various methods
* that require a character set to be provided.
*
* @see http://php.net/htmlspecialchars for a list of supported charsets.
*/
public string $charset = 'UTF-8';
/**
* --------------------------------------------------------------------------
* Force Global Secure Requests
* --------------------------------------------------------------------------
*
* If true, this will force every request made to this application to be
* made via a secure connection (HTTPS). If the incoming request is not
* secure, the user will be redirected to a secure version of the page
* and the HTTP Strict Transport Security header will be set.
*/
public bool $forceGlobalSecureRequests = false;
/**
* --------------------------------------------------------------------------
* Reverse Proxy IPs
* --------------------------------------------------------------------------
*
* If your server is behind a reverse proxy, you must whitelist the proxy
* IP addresses from which CodeIgniter should trust headers such as
* X-Forwarded-For or Client-IP in order to properly identify
* the visitor's IP address.
*
* You need to set a proxy IP address or IP address with subnets and
* the HTTP header for the client IP address.
*
* Here are some examples:
* [
* '10.0.1.200' => 'X-Forwarded-For',
* '192.168.5.0/24' => 'X-Real-IP',
* ]
*
* @var array<string, string>
*/
public array $proxyIPs = [];
/**
* --------------------------------------------------------------------------
* Content Security Policy
* --------------------------------------------------------------------------
*
* Enables the Response's Content Secure Policy to restrict the sources that
* can be used for images, scripts, CSS files, audio, video, etc. If enabled,
* the Response object will populate default values for the policy from the
* `ContentSecurityPolicy.php` file. Controllers can always add to those
* restrictions at run time.
*
* For a better understanding of CSP, see these documents:
*
* @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/
* @see http://www.w3.org/TR/CSP/
*/
public bool $CSPEnabled = false;
}

106
app/Config/Autoload.php Executable file
View File

@ -0,0 +1,106 @@
<?php
namespace Config;
use CodeIgniter\Config\AutoloadConfig;
/**
* -------------------------------------------------------------------
* AUTOLOADER CONFIGURATION
* -------------------------------------------------------------------
*
* This file defines the namespaces and class maps so the Autoloader
* can find the files as needed.
*
* NOTE: If you use an identical key in $psr4 or $classmap, then
* the values in this file will overwrite the framework's values.
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*
* @immutable
*/
class Autoload extends AutoloadConfig
{
/**
* -------------------------------------------------------------------
* Namespaces
* -------------------------------------------------------------------
* This maps the locations of any namespaces in your application to
* their location on the file system. These are used by the autoloader
* to locate files the first time they have been instantiated.
*
* The '/app' and '/system' directories are already mapped for you.
* you may change the name of the 'App' namespace if you wish,
* but this should be done prior to creating any namespaced classes,
* else you will need to modify all of those classes for this to work.
*
* Prototype:
* $psr4 = [
* 'CodeIgniter' => SYSTEMPATH,
* 'App' => APPPATH
* ];
*
* @var array<string, array<int, string>|string>
* @phpstan-var array<string, string|list<string>>
*/
public $psr4 = [
APP_NAMESPACE => APPPATH, // For custom app namespace
'Config' => APPPATH . 'Config',
'Helpers' => APPPATH . 'Helpers',
'App\\Libraries' => APPPATH . 'Libraries',
];
/**
* -------------------------------------------------------------------
* Class Map
* -------------------------------------------------------------------
* The class map provides a map of class names and their exact
* location on the drive. Classes loaded in this manner will have
* slightly faster performance because they will not have to be
* searched for within one or more directories as they would if they
* were being autoloaded through a namespace.
*
* Prototype:
* $classmap = [
* 'MyClass' => '/path/to/class/file.php'
* ];
*
* @var array<string, string>
*/
public $classmap = [];
/**
* -------------------------------------------------------------------
* Files
* -------------------------------------------------------------------
* The files array provides a list of paths to __non-class__ files
* that will be autoloaded. This can be useful for bootstrap operations
* or for loading functions.
*
* Prototype:
* $files = [
* '/path/to/my/file.php',
* ];
*
* @var string[]
* @phpstan-var list<string>
*/
public $files = [];
/**
* -------------------------------------------------------------------
* Helpers
* -------------------------------------------------------------------
* Prototype:
* $helpers = [
* 'form',
* ];
*
* @var string[]
* @phpstan-var list<string>
*/
public $helpers = ['uuid','session','utility', 'form', 'url', 'oauth', 'fileupload',
'excel_import_export', 'file', 'drive','ExcelSanitizeHelper', 'api_helper','exception','sms_helper','sanitizeInputArrayAdvanced'
];
}

64
app/Config/BdsConfig.php Normal file
View File

@ -0,0 +1,64 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class BdsConfig extends BaseConfig
{
public bool $installmentReminderEnabled = false;
public int $installmentReminderBusinessDays = 5;
public bool $installmentReminderFetchOverduePendingUtr = false;
public bool $installmentReminderDaily = false;
/** @var string[] Lowercase three-letter day abbreviations, e.g. mon, tue */
public array $installmentReminderDays = [];
public function __construct()
{
parent::__construct();
$this->installmentReminderEnabled = $this->envToBool(
env('bds.installmentReminder.enabled', 'true')
);
$this->installmentReminderBusinessDays = max(
0,
(int) env('bds.installmentReminder.businessDays', 5)
);
$this->installmentReminderFetchOverduePendingUtr = $this->envToBool(
env('bds.installmentReminder.fetchOverduePendingUtr', 'false')
);
$this->installmentReminderDaily = $this->envToBool(
env('bds.installmentReminder.daily', 'true')
);
$days = env('bds.installmentReminder.days', 'mon,tue,wed,thu,fri');
$this->installmentReminderDays = array_values(array_filter(array_map(
static fn (string $day): string => strtolower(substr(trim($day), 0, 3)),
explode(',', (string) $days)
)));
}
public function shouldFetchInstallmentRemindersToday(): bool
{
if (!$this->installmentReminderEnabled) {
return false;
}
if ($this->installmentReminderDaily) {
return true;
}
$today = strtolower(date('D'));
return in_array($today, $this->installmentReminderDays, true);
}
private function envToBool(mixed $value): bool
{
return in_array(strtolower((string) $value), ['1', 'true', 'yes', 'on'], true);
}
}

34
app/Config/Boot/development.php Executable file
View File

@ -0,0 +1,34 @@
<?php
/*
|--------------------------------------------------------------------------
| ERROR DISPLAY
|--------------------------------------------------------------------------
| In development, we want to show as many errors as possible to help
| make sure they don't make it to production. And save us hours of
| painful debugging.
|
| If you set 'display_errors' to '1', CI4's detailed error report will show.
*/
error_reporting(E_ALL);
ini_set('display_errors', '1');
/*
|--------------------------------------------------------------------------
| DEBUG BACKTRACES
|--------------------------------------------------------------------------
| If true, this constant will tell the error screens to display debug
| backtraces along with the other error information. If you would
| prefer to not see this, set this value to false.
*/
defined('SHOW_DEBUG_BACKTRACE') || define('SHOW_DEBUG_BACKTRACE', true);
/*
|--------------------------------------------------------------------------
| DEBUG MODE
|--------------------------------------------------------------------------
| Debug mode is an experimental flag that can allow changes throughout
| the system. This will control whether Kint is loaded, and a few other
| items. It can always be used within your own application too.
*/
defined('CI_DEBUG') || define('CI_DEBUG', true);

23
app/Config/Boot/production.php Executable file
View File

@ -0,0 +1,23 @@
<?php
/*
|--------------------------------------------------------------------------
| ERROR DISPLAY
|--------------------------------------------------------------------------
| Don't show ANY in production environments. Instead, let the system catch
| it and display a generic error message.
|
| If you set 'display_errors' to '1', CI4's detailed error report will show.
*/
ini_set('display_errors', '0');
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED);
/*
|--------------------------------------------------------------------------
| DEBUG MODE
|--------------------------------------------------------------------------
| Debug mode is an experimental flag that can allow changes throughout
| the system. It's not widely used currently, and may not survive
| release of the framework.
*/
defined('CI_DEBUG') || define('CI_DEBUG', false);

38
app/Config/Boot/test.php Executable file
View File

@ -0,0 +1,38 @@
<?php
/*
* The environment testing is reserved for PHPUnit testing. It has special
* conditions built into the framework at various places to assist with that.
* You cant use it for your development.
*/
/*
|--------------------------------------------------------------------------
| ERROR DISPLAY
|--------------------------------------------------------------------------
| In development, we want to show as many errors as possible to help
| make sure they don't make it to production. And save us hours of
| painful debugging.
*/
error_reporting(E_ALL);
ini_set('display_errors', '1');
/*
|--------------------------------------------------------------------------
| DEBUG BACKTRACES
|--------------------------------------------------------------------------
| If true, this constant will tell the error screens to display debug
| backtraces along with the other error information. If you would
| prefer to not see this, set this value to false.
*/
defined('SHOW_DEBUG_BACKTRACE') || define('SHOW_DEBUG_BACKTRACE', true);
/*
|--------------------------------------------------------------------------
| DEBUG MODE
|--------------------------------------------------------------------------
| Debug mode is an experimental flag that can allow changes throughout
| the system. It's not widely used currently, and may not survive
| release of the framework.
*/
defined('CI_DEBUG') || define('CI_DEBUG', true);

38
app/Config/Boot/testing.php Executable file
View File

@ -0,0 +1,38 @@
<?php
/*
* The environment testing is reserved for PHPUnit testing. It has special
* conditions built into the framework at various places to assist with that.
* You cant use it for your development.
*/
/*
|--------------------------------------------------------------------------
| ERROR DISPLAY
|--------------------------------------------------------------------------
| In development, we want to show as many errors as possible to help
| make sure they don't make it to production. And save us hours of
| painful debugging.
*/
error_reporting(E_ALL);
ini_set('display_errors', '1');
/*
|--------------------------------------------------------------------------
| DEBUG BACKTRACES
|--------------------------------------------------------------------------
| If true, this constant will tell the error screens to display debug
| backtraces along with the other error information. If you would
| prefer to not see this, set this value to false.
*/
defined('SHOW_DEBUG_BACKTRACE') || define('SHOW_DEBUG_BACKTRACE', true);
/*
|--------------------------------------------------------------------------
| DEBUG MODE
|--------------------------------------------------------------------------
| Debug mode is an experimental flag that can allow changes throughout
| the system. It's not widely used currently, and may not survive
| release of the framework.
*/
defined('CI_DEBUG') || define('CI_DEBUG', true);

38
app/Config/Boot/uat.php Executable file
View File

@ -0,0 +1,38 @@
<?php
/*
* The environment testing is reserved for PHPUnit testing. It has special
* conditions built into the framework at various places to assist with that.
* You cant use it for your development.
*/
/*
|--------------------------------------------------------------------------
| ERROR DISPLAY
|--------------------------------------------------------------------------
| In development, we want to show as many errors as possible to help
| make sure they don't make it to production. And save us hours of
| painful debugging.
*/
error_reporting(E_ALL);
ini_set('display_errors', '1');
/*
|--------------------------------------------------------------------------
| DEBUG BACKTRACES
|--------------------------------------------------------------------------
| If true, this constant will tell the error screens to display debug
| backtraces along with the other error information. If you would
| prefer to not see this, set this value to false.
*/
defined('SHOW_DEBUG_BACKTRACE') || define('SHOW_DEBUG_BACKTRACE', true);
/*
|--------------------------------------------------------------------------
| DEBUG MODE
|--------------------------------------------------------------------------
| Debug mode is an experimental flag that can allow changes throughout
| the system. It's not widely used currently, and may not survive
| release of the framework.
*/
defined('CI_DEBUG') || define('CI_DEBUG', true);

20
app/Config/CURLRequest.php Executable file
View File

@ -0,0 +1,20 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class CURLRequest extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* CURLRequest Share Options
* --------------------------------------------------------------------------
*
* Whether share options between requests or not.
*
* If true, all the options won't be reset between requests.
* It may cause an error request with unnecessary headers.
*/
public bool $shareOptions = false;
}

171
app/Config/Cache.php Executable file
View File

@ -0,0 +1,171 @@
<?php
namespace Config;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Cache\Handlers\DummyHandler;
use CodeIgniter\Cache\Handlers\FileHandler;
use CodeIgniter\Cache\Handlers\MemcachedHandler;
use CodeIgniter\Cache\Handlers\PredisHandler;
use CodeIgniter\Cache\Handlers\RedisHandler;
use CodeIgniter\Cache\Handlers\WincacheHandler;
use CodeIgniter\Config\BaseConfig;
class Cache extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Primary Handler
* --------------------------------------------------------------------------
*
* The name of the preferred handler that should be used. If for some reason
* it is not available, the $backupHandler will be used in its place.
*/
public string $handler = 'file';
/**
* --------------------------------------------------------------------------
* Backup Handler
* --------------------------------------------------------------------------
*
* The name of the handler that will be used in case the first one is
* unreachable. Often, 'file' is used here since the filesystem is
* always available, though that's not always practical for the app.
*/
public string $backupHandler = 'dummy';
/**
* --------------------------------------------------------------------------
* Cache Directory Path
* --------------------------------------------------------------------------
*
* The path to where cache files should be stored, if using a file-based
* system.
*
* @deprecated Use the driver-specific variant under $file
*/
public string $storePath = WRITEPATH . 'cache/';
/**
* --------------------------------------------------------------------------
* Cache Include Query String
* --------------------------------------------------------------------------
*
* Whether to take the URL query string into consideration when generating
* output cache files. Valid options are:
*
* false = Disabled
* true = Enabled, take all query parameters into account.
* Please be aware that this may result in numerous cache
* files generated for the same page over and over again.
* ['q'] = Enabled, but only take into account the specified list
* of query parameters.
*
* @var bool|string[]
*/
public $cacheQueryString = false;
/**
* --------------------------------------------------------------------------
* Key Prefix
* --------------------------------------------------------------------------
*
* This string is added to all cache item names to help avoid collisions
* if you run multiple applications with the same cache engine.
*/
public string $prefix = '';
/**
* --------------------------------------------------------------------------
* Default TTL
* --------------------------------------------------------------------------
*
* The default number of seconds to save items when none is specified.
*
* WARNING: This is not used by framework handlers where 60 seconds is
* hard-coded, but may be useful to projects and modules. This will replace
* the hard-coded value in a future release.
*/
public int $ttl = 60;
/**
* --------------------------------------------------------------------------
* Reserved Characters
* --------------------------------------------------------------------------
*
* A string of reserved characters that will not be allowed in keys or tags.
* Strings that violate this restriction will cause handlers to throw.
* Default: {}()/\@:
*
* NOTE: The default set is required for PSR-6 compliance.
*/
public string $reservedCharacters = '{}()/\@:';
/**
* --------------------------------------------------------------------------
* File settings
* --------------------------------------------------------------------------
* Your file storage preferences can be specified below, if you are using
* the File driver.
*
* @var array<string, int|string|null>
*/
public array $file = [
'storePath' => WRITEPATH . 'cache/',
'mode' => 0640,
];
/**
* -------------------------------------------------------------------------
* Memcached settings
* -------------------------------------------------------------------------
* Your Memcached servers can be specified below, if you are using
* the Memcached drivers.
*
* @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
*
* @var array<string, bool|int|string>
*/
public array $memcached = [
'host' => '127.0.0.1',
'port' => 11211,
'weight' => 1,
'raw' => false,
];
/**
* -------------------------------------------------------------------------
* Redis settings
* -------------------------------------------------------------------------
* Your Redis server can be specified below, if you are using
* the Redis or Predis drivers.
*
* @var array<string, int|string|null>
*/
public array $redis = [
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
'database' => 0,
];
/**
* --------------------------------------------------------------------------
* Available Cache Handlers
* --------------------------------------------------------------------------
*
* This is an array of cache engine alias' and class names. Only engines
* that are listed here are allowed to be used.
*
* @var array<string, class-string<CacheInterface>>
*/
public array $validHandlers = [
'dummy' => DummyHandler::class,
'file' => FileHandler::class,
'memcached' => MemcachedHandler::class,
'predis' => PredisHandler::class,
'redis' => RedisHandler::class,
'wincache' => WincacheHandler::class,
];
}

137
app/Config/Constants.php Executable file
View File

@ -0,0 +1,137 @@
<?php
/*
| --------------------------------------------------------------------
| App Namespace
| --------------------------------------------------------------------
|
| This defines the default Namespace that is used throughout
| CodeIgniter to refer to the Application directory. Change
| this constant to change the namespace that all application
| classes should use.
|
| NOTE: changing this will require manually modifying the
| existing namespaces of App\* namespaced-classes.
*/
defined('APP_NAMESPACE') || define('APP_NAMESPACE', 'App');
/*
| --------------------------------------------------------------------------
| Composer Path
| --------------------------------------------------------------------------
|
| The path that Composer's autoload file is expected to live. By default,
| the vendor folder is in the Root directory, but you can customize that here.
*/
defined('COMPOSER_PATH') || define('COMPOSER_PATH', ROOTPATH . 'vendor/autoload.php');
/*
|--------------------------------------------------------------------------
| Timing Constants
|--------------------------------------------------------------------------
|
| Provide simple ways to work with the myriad of PHP functions that
| require information to be in seconds.
*/
defined('SECOND') || define('SECOND', 1);
defined('MINUTE') || define('MINUTE', 60);
defined('HOUR') || define('HOUR', 3600);
defined('DAY') || define('DAY', 86400);
defined('WEEK') || define('WEEK', 604800);
defined('MONTH') || define('MONTH', 2_592_000);
defined('YEAR') || define('YEAR', 31_536_000);
defined('DECADE') || define('DECADE', 315_360_000);
/*
| --------------------------------------------------------------------------
| Exit Status Codes
| --------------------------------------------------------------------------
|
| Used to indicate the conditions under which the script is exit()ing.
| While there is no universal standard for error codes, there are some
| broad conventions. Three such conventions are mentioned below, for
| those who wish to make use of them. The CodeIgniter defaults were
| chosen for the least overlap with these conventions, while still
| leaving room for others to be defined in future versions and user
| applications.
|
| The three main conventions used for determining exit status codes
| are as follows:
|
| Standard C/C++ Library (stdlibc):
| http://www.gnu.org/software/libc/manual/html_node/Exit-Status.html
| (This link also contains other GNU-specific conventions)
| BSD sysexits.h:
| http://www.gsp.com/cgi-bin/man.cgi?section=3&topic=sysexits
| Bash scripting:
| http://tldp.org/LDP/abs/html/exitcodes.html
|
*/
defined('EXIT_SUCCESS') || define('EXIT_SUCCESS', 0); // no errors
defined('EXIT_ERROR') || define('EXIT_ERROR', 1); // generic error
defined('EXIT_CONFIG') || define('EXIT_CONFIG', 3); // configuration error
defined('EXIT_UNKNOWN_FILE') || define('EXIT_UNKNOWN_FILE', 4); // file not found
defined('EXIT_UNKNOWN_CLASS') || define('EXIT_UNKNOWN_CLASS', 5); // unknown class
defined('EXIT_UNKNOWN_METHOD') || define('EXIT_UNKNOWN_METHOD', 6); // unknown class member
defined('EXIT_USER_INPUT') || define('EXIT_USER_INPUT', 7); // invalid user input
defined('EXIT_DATABASE') || define('EXIT_DATABASE', 8); // database error
defined('EXIT__AUTO_MIN') || define('EXIT__AUTO_MIN', 9); // lowest automatically-assigned error code
defined('EXIT__AUTO_MAX') || define('EXIT__AUTO_MAX', 125); // highest automatically-assigned error code
/**
* @deprecated Use \CodeIgniter\Events\Events::PRIORITY_LOW instead.
*/
define('EVENT_PRIORITY_LOW', 200);
/**
* @deprecated Use \CodeIgniter\Events\Events::PRIORITY_NORMAL instead.
*/
define('EVENT_PRIORITY_NORMAL', 100);
/**
* @deprecated Use \CodeIgniter\Events\Events::PRIORITY_HIGH instead.
*/
define('EVENT_PRIORITY_HIGH', 10);
/**
* @User Teams Constant
*/
define('ENROLLMENT_TEAM_ID', '1');
define('CLAIMS_TEAM_ID', '2');
// define('BUSINESS_TEAM_ID', '3'); // old don't delete it
define('BUSINESS_TEAM_ID', '4'); // 25th OCT 2025 changed
define('FINANCE_TEAM_ID', '4');
define('SALES_TEAM_ID', '5');
define('MANAGEMENT_TEAM_ID', '6');
define('BUSINESS_SUPPORT_TEAM_ID', '7');
define('POS_TEAM_ID', '8');
/**
* @User Role Constant
*/
define('ADMIN_ROLE_ID', 1);
define('MANAGER_ROLE_ID', 2);
define('ACCOUNT_MANAGER_ROLE_ID', 3);
define('STAFF_ROLE_ID', 4);
define('HEAD_ROLE_ID', 5);
/**
* @Upload Allowed Extensions by Business Context
*/
define('UPLOAD_ALLOWED_EXTENSIONS', [
'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg',
'pdf', 'doc', 'docx', 'odt', 'rtf',
'xls', 'xlsx', 'ods', 'csv', 'txt',
]);
define('UPLOAD_EXT_IMAGES', ['jpg', 'jpeg', 'png']);
define('UPLOAD_EXT_KYC_DOCS', ['pdf', 'jpg', 'jpeg', 'png']);
define('UPLOAD_EXT_CLAIM_DOCS', ['pdf', 'jpg', 'jpeg', 'png']);
define('UPLOAD_EXT_POLICY_DOCS', ['pdf', 'jpg', 'jpeg', 'png', 'xls', 'xlsx']);
define('UPLOAD_EXT_LEAD_FILES', ['xls', 'xlsx', 'pdf', 'jpg', 'jpeg', 'png']);
define('UPLOAD_EXT_EXCEL', ['xls', 'xlsx', 'ods', 'csv']);
define('UPLOAD_EXT_MAIL_ATTACHMENTS', ['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx', 'xls', 'xlsx']);
define('UPLOAD_EXT_NON_EB_RACK_RATE', ['pdf', 'xls', 'xlsx']);
define('UPLOAD_EXT_ASSET_FILES', ['pdf', 'xls', 'xlsx', 'csv']);
define('UPLOAD_EXT_INSURER_CLAIM_FORM', ['pdf', 'doc', 'docx']);
define('INSURER_DEFAULT_CLAIM_FORM_FILE', 'Claim_Form_IRDAI.pdf');

View File

@ -0,0 +1,176 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
/**
* Stores the default settings for the ContentSecurityPolicy, if you
* choose to use it. The values here will be read in and set as defaults
* for the site. If needed, they can be overridden on a page-by-page basis.
*
* Suggested reference for explanations:
*
* @see https://www.html5rocks.com/en/tutorials/security/content-security-policy/
*/
class ContentSecurityPolicy extends BaseConfig
{
// -------------------------------------------------------------------------
// Broadbrush CSP management
// -------------------------------------------------------------------------
/**
* Default CSP report context
*/
public bool $reportOnly = false;
/**
* Specifies a URL where a browser will send reports
* when a content security policy is violated.
*/
public ?string $reportURI = null;
/**
* Instructs user agents to rewrite URL schemes, changing
* HTTP to HTTPS. This directive is for websites with
* large numbers of old URLs that need to be rewritten.
*/
public bool $upgradeInsecureRequests = false;
// -------------------------------------------------------------------------
// Sources allowed
// NOTE: once you set a policy to 'none', it cannot be further restricted
// -------------------------------------------------------------------------
/**
* Will default to self if not overridden
*
* @var string|string[]|null
*/
public $defaultSrc;
/**
* Lists allowed scripts' URLs.
*
* @var string|string[]
*/
public $scriptSrc = 'self';
/**
* Lists allowed stylesheets' URLs.
*
* @var string|string[]
*/
public $styleSrc = 'self';
/**
* Defines the origins from which images can be loaded.
*
* @var string|string[]
*/
public $imageSrc = 'self';
/**
* Restricts the URLs that can appear in a page's `<base>` element.
*
* Will default to self if not overridden
*
* @var string|string[]|null
*/
public $baseURI;
/**
* Lists the URLs for workers and embedded frame contents
*
* @var string|string[]
*/
public $childSrc = 'self';
/**
* Limits the origins that you can connect to (via XHR,
* WebSockets, and EventSource).
*
* @var string|string[]
*/
public $connectSrc = 'self';
/**
* Specifies the origins that can serve web fonts.
*
* @var string|string[]
*/
public $fontSrc;
/**
* Lists valid endpoints for submission from `<form>` tags.
*
* @var string|string[]
*/
public $formAction = 'self';
/**
* Specifies the sources that can embed the current page.
* This directive applies to `<frame>`, `<iframe>`, `<embed>`,
* and `<applet>` tags. This directive can't be used in
* `<meta>` tags and applies only to non-HTML resources.
*
* @var string|string[]|null
*/
public $frameAncestors;
/**
* The frame-src directive restricts the URLs which may
* be loaded into nested browsing contexts.
*
* @var array|string|null
*/
public $frameSrc;
/**
* Restricts the origins allowed to deliver video and audio.
*
* @var string|string[]|null
*/
public $mediaSrc;
/**
* Allows control over Flash and other plugins.
*
* @var string|string[]
*/
public $objectSrc = 'self';
/**
* @var string|string[]|null
*/
public $manifestSrc;
/**
* Limits the kinds of plugins a page may invoke.
*
* @var string|string[]|null
*/
public $pluginTypes;
/**
* List of actions allowed.
*
* @var string|string[]|null
*/
public $sandbox;
/**
* Nonce tag for style
*/
public string $styleNonceTag = '{csp-style-nonce}';
/**
* Nonce tag for script
*/
public string $scriptNonceTag = '{csp-script-nonce}';
/**
* Replace nonce tag automatically
*/
public bool $autoNonce = true;
}

107
app/Config/Cookie.php Executable file
View File

@ -0,0 +1,107 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
use DateTimeInterface;
class Cookie extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Cookie Prefix
* --------------------------------------------------------------------------
*
* Set a cookie name prefix if you need to avoid collisions.
*/
public string $prefix = '';
/**
* --------------------------------------------------------------------------
* Cookie Expires Timestamp
* --------------------------------------------------------------------------
*
* Default expires timestamp for cookies. Setting this to `0` will mean the
* cookie will not have the `Expires` attribute and will behave as a session
* cookie.
*
* @var DateTimeInterface|int|string
*/
public $expires = 0;
/**
* --------------------------------------------------------------------------
* Cookie Path
* --------------------------------------------------------------------------
*
* Typically will be a forward slash.
*/
public string $path = '/';
/**
* --------------------------------------------------------------------------
* Cookie Domain
* --------------------------------------------------------------------------
*
* Set to `.your-domain.com` for site-wide cookies.
*/
public string $domain = '';
/**
* --------------------------------------------------------------------------
* Cookie Secure
* --------------------------------------------------------------------------
*
* Cookie will only be set if a secure HTTPS connection exists.
*/
public bool $secure = false;
/**
* --------------------------------------------------------------------------
* Cookie HTTPOnly
* --------------------------------------------------------------------------
*
* Cookie will only be accessible via HTTP(S) (no JavaScript).
*/
public bool $httponly = true;
/**
* --------------------------------------------------------------------------
* Cookie SameSite
* --------------------------------------------------------------------------
*
* Configure cookie SameSite setting. Allowed values are:
* - None
* - Lax
* - Strict
* - ''
*
* Alternatively, you can use the constant names:
* - `Cookie::SAMESITE_NONE`
* - `Cookie::SAMESITE_LAX`
* - `Cookie::SAMESITE_STRICT`
*
* Defaults to `Lax` for compatibility with modern browsers. Setting `''`
* (empty string) means default SameSite attribute set by browsers (`Lax`)
* will be set on cookies. If set to `None`, `$secure` must also be set.
*
* @phpstan-var 'None'|'Lax'|'Strict'|''
*/
public string $samesite = 'Lax';
/**
* --------------------------------------------------------------------------
* Cookie Raw
* --------------------------------------------------------------------------
*
* This flag allows setting a "raw" cookie, i.e., its name and value are
* not URL encoded using `rawurlencode()`.
*
* If this is set to `true`, cookie names should be compliant of RFC 2616's
* list of allowed characters.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes
* @see https://tools.ietf.org/html/rfc2616#section-2.2
*/
public bool $raw = false;
}

144
app/Config/Database.php Executable file
View File

@ -0,0 +1,144 @@
<?php
namespace Config;
use CodeIgniter\Database\Config;
/**
* Database Configuration
*/
class Database extends Config
{
/**
* The directory that holds the Migrations
* and Seeds directories.
*/
public string $filesPath = APPPATH . 'Database' . DIRECTORY_SEPARATOR;
/**
* Lets you choose which connection group to
* use if no other is specified.
*/
public string $defaultGroup = 'default';
public string $enableSSL;
/**
* The default database connection.
*/
public array $default = [
'DSN' => '',
'hostname' => 'localhost',
'username' => '',
'password' => '',
'database' => '',
'DBDriver' => 'MySQLi',
'DBPrefix' => '',
'pConnect' => false,
'DBDebug' => true,
'charset' => 'utf8',
'DBCollat' => 'utf8_general_ci',
'swapPre' => '',
'encrypt' =>false,
'compress' => false,
'strictOn' => false,
'failover' => [],
'port' => 3306,
'numberNative' => false,
];
public array $book_stack = [];
/**
* This database connection is used when
* running PHPUnit database tests.
*/
public array $tests = [
'DSN' => '',
'hostname' => '119.18.54.85',
'username' => 'venbaehn_nhance_user1',
'password' => 'iM~X7(cR+}A-',
'database' => 'venbaehn_nhance',
'DBDriver' => 'SQLite3',
'DBPrefix' => 'db_', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS
'pConnect' => false,
'DBDebug' => true,
'charset' => 'utf8',
'DBCollat' => 'utf8_general_ci',
'swapPre' => '',
'encrypt' => false,
'compress' => false,
'strictOn' => false,
'failover' => [],
'port' => 3306,
'foreignKeys' => true,
'busyTimeout' => 1000,
];
public $preDB = [
'DSN' => '',
'hostname' => 'localhost',
'username' => 'root',
'password' => 'root',
'database' => 'other_db',
'DBDriver' => 'MySQLi',
'DBPrefix' => '',
'pConnect' => false,
'DBDebug' => (ENVIRONMENT !== 'production'),
'charset' => 'utf8',
'DBCollat' => 'utf8_general_ci',
];
public function __construct()
{
parent::__construct();
// Initialize book_stack configuration with environment variables
$this->book_stack = [
'DSN' => '',
'hostname' => getenv('bookStack_hostname'),
'username' => getenv('bookStack_username'),
'password' => getenv('bookStack_password'),
'database' => getenv('bookStack_database'),
'DBDriver' => 'MySQLi',
'DBPrefix' => '',
'pConnect' => false,
'DBDebug' => true,
'charset' => 'utf8',
'DBCollat' => 'utf8_general_ci',
'swapPre' => '',
'encrypt' => false,
'compress' => false,
'strictOn' => false,
'failover' => [],
'port' => 3306,
'foreignKeys' => true,
'busyTimeout' => 1000,
];
// Ensure that we always set the database group to 'tests' if
// we are currently running an automated test suite, so that
// we don't overwrite live data on accident.
if (ENVIRONMENT === 'testing') {
$this->defaultGroup = 'tests';
}
$this->enableSSL = env('DB_SSL_ENABLE') ?? false;
if ($this->enableSSL === true || $this->enableSSL === 'true' || $this->enableSSL === 1 || $this->enableSSL === '1') {
// Enable SSL authentication
$this->default['encrypt'] = [
'ssl_ca' => ROOTPATH . 'ca.pem',
'ssl_verify' => true,
];
$this->preDB['encrypt'] = [
'ssl_ca' => ROOTPATH . 'ca.pem',
'ssl_verify' => true,
];
}
}
}

45
app/Config/DigitMotor.php Normal file
View File

@ -0,0 +1,45 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class DigitMotor extends BaseConfig
{
public string $baseUrl;
public string $authPath;
public string $executorPath;
public string $username;
public string $password;
public string $environment;
public int $timeout;
public int $tokenLeewaySec;
/** Partner key required by Digit policy PDF headerParam.Authorization */
public string $pdfAuthKey;
public array $integrationIds;
public function __construct()
{
parent::__construct();
$this->baseUrl = rtrim((string) env('DIGIT_MOTOR_BASE_URL', 'https://preprod-oneapi.godigit.com'), '/');
$this->authPath = (string) env('DIGIT_MOTOR_AUTH_PATH', '/OneAPI/v1/auth');
$this->executorPath = (string) env('DIGIT_MOTOR_EXECUTOR_PATH', '/OneAPI/v1/executor');
$this->username = (string) env('DIGIT_MOTOR_USERNAME', '');
$this->password = (string) env('DIGIT_MOTOR_PASSWORD', '');
$this->environment = (string) env('DIGIT_MOTOR_ENVIRONMENT', 'staging');
$this->timeout = (int) env('DIGIT_MOTOR_TIMEOUT', 30);
$this->tokenLeewaySec = (int) env('DIGIT_MOTOR_TOKEN_LEEWAY_SEC', 60);
$this->pdfAuthKey = (string) env('DIGIT_MOTOR_PDF_AUTH_KEY', '');
// Integration IDs from NHANCE Digit API kit (CURL / Bruno export)
$this->integrationIds = [
'quickQuote' => (string) env('DIGIT_MOTOR_IID_QUICK_QUOTE', '29266-0100'),
'createQuote' => (string) env('DIGIT_MOTOR_IID_CREATE_QUOTE', '29268-0100'),
'kycStatus' => (string) env('DIGIT_MOTOR_IID_KYC', '29269-0100'),
'payment' => (string) env('DIGIT_MOTOR_IID_PAYMENT', '29270-0100'),
'policyStatus' => (string) env('DIGIT_MOTOR_IID_POLICY_STATUS', '29271-0100'),
'policyPdf' => (string) env('DIGIT_MOTOR_IID_POLICY_PDF', '29272-0100'),
];
}
}

46
app/Config/DocTypes.php Executable file
View File

@ -0,0 +1,46 @@
<?php
namespace Config;
/**
* @immutable
*/
class DocTypes
{
/**
* List of valid document types.
*
* @var array<string, string>
*/
public array $list = [
'xhtml11' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
'xhtml1-strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
'xhtml1-trans' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
'xhtml1-frame' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
'xhtml-basic11' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
'html5' => '<!DOCTYPE html>',
'html4-strict' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
'html4-trans' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
'html4-frame' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">',
'mathml1' => '<!DOCTYPE math SYSTEM "http://www.w3.org/Math/DTD/mathml1/mathml.dtd">',
'mathml2' => '<!DOCTYPE math PUBLIC "-//W3C//DTD MathML 2.0//EN" "http://www.w3.org/Math/DTD/mathml2/mathml2.dtd">',
'svg10' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">',
'svg11' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
'svg11-basic' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Basic//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd">',
'svg11-tiny' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd">',
'xhtml-math-svg-xh' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">',
'xhtml-math-svg-sh' => '<!DOCTYPE svg:svg PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">',
'xhtml-rdfa-1' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">',
'xhtml-rdfa-2' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.1//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-2.dtd">',
];
/**
* Whether to remove the solidus (`/`) character for void HTML elements (e.g. `<input>`)
* for HTML5 compatibility.
*
* Set to:
* `true` - to be HTML5 compatible
* `false` - to be XHTML compatible
*/
public bool $html5 = true;
}

131
app/Config/Email.php Executable file
View File

@ -0,0 +1,131 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class Email extends BaseConfig
{
public string $fromEmail = '';
public string $fromName = '';
public string $recipients = '';
/**
* The "user agent"
*/
public string $userAgent = 'CodeIgniter';
/**
* The mail sending protocol: mail, sendmail, smtp
*/
public string $protocol = 'smtp';
/**
* The server path to Sendmail.
*/
public string $mailPath = '/usr/sbin/sendmail';
/**
* SMTP Server Hostname
*/
public string $SMTPHost = 'smtp.gmail.com';
/**
* SMTP Username
*/
public string $SMTPUser = 'venbatechnologies@gmail.com';
/**
* SMTP Password
*/
public string $SMTPPass = 'dsfllmxdzxxkcxgz';
/**
* SMTP Port
*/
public int $SMTPPort = 465;
/**
* SMTP Timeout (in seconds)
*/
public int $SMTPTimeout = 1000;
/**
* Enable persistent SMTP connections
*/
public bool $SMTPKeepAlive = false;
/**
* SMTP Encryption.
*
* @var string '', 'tls' or 'ssl'. 'tls' will issue a STARTTLS command
* to the server. 'ssl' means implicit SSL. Connection on port
* 465 should set this to ''.
*/
public string $SMTPCrypto = 'ssl';
/**
* Enable word-wrap
*/
public bool $wordWrap = true;
/**
* Character count to wrap at
*/
public int $wrapChars = 76;
/**
* Type of mail, either 'text' or 'html'
*/
public string $mailType = 'html';
/**
* Character set (utf-8, iso-8859-1, etc.)
*/
public string $charset = 'UTF-8';
/**
* Whether to validate the email address
*/
public bool $validate = false;
/**
* Email Priority. 1 = highest. 5 = lowest. 3 = normal
*/
public int $priority = 3;
/**
* Newline character. (Use “\r\n” to comply with RFC 822)
*/
public string $CRLF = "\r\n";
/**
* Newline character. (Use “\r\n” to comply with RFC 822)
*/
public string $newline = "\r\n";
/**
* Enable BCC Batch Mode.
*/
public bool $BCCBatchMode = false;
/**
* Number of emails in each BCC batch
*/
public int $BCCBatchSize = 200;
/**
* Enable notify message from server
*/
public bool $DSN = false;
/**
* SMTP Debugging level. Set to 0 to disable debugging.
* Set to 1 to output server connection status only.
* Set to 2 for more detailed debugging output.
* Set to 3 for maximum debugging output.
*/
public int $SMTPDebug = 2;
}

92
app/Config/Encryption.php Executable file
View File

@ -0,0 +1,92 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
/**
* Encryption configuration.
*
* These are the settings used for encryption, if you don't pass a parameter
* array to the encrypter for creation/initialization.
*/
class Encryption extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Encryption Key Starter
* --------------------------------------------------------------------------
*
* If you use the Encryption class you must set an encryption key (seed).
* You need to ensure it is long enough for the cipher and mode you plan to use.
* See the user guide for more info.
*/
public string $key = '';
/**
* --------------------------------------------------------------------------
* Encryption Driver to Use
* --------------------------------------------------------------------------
*
* One of the supported encryption drivers.
*
* Available drivers:
* - OpenSSL
* - Sodium
*/
public string $driver = 'OpenSSL';
/**
* --------------------------------------------------------------------------
* SodiumHandler's Padding Length in Bytes
* --------------------------------------------------------------------------
*
* This is the number of bytes that will be padded to the plaintext message
* before it is encrypted. This value should be greater than zero.
*
* See the user guide for more information on padding.
*/
public int $blockSize = 16;
/**
* --------------------------------------------------------------------------
* Encryption digest
* --------------------------------------------------------------------------
*
* HMAC digest to use, e.g. 'SHA512' or 'SHA256'. Default value is 'SHA512'.
*/
public string $digest = 'SHA512';
/**
* Whether the cipher-text should be raw. If set to false, then it will be base64 encoded.
* This setting is only used by OpenSSLHandler.
*
* Set to false for CI3 Encryption compatibility.
*/
public bool $rawData = true;
/**
* Encryption key info.
* This setting is only used by OpenSSLHandler.
*
* Set to 'encryption' for CI3 Encryption compatibility.
*/
public string $encryptKeyInfo = '';
/**
* Authentication key info.
* This setting is only used by OpenSSLHandler.
*
* Set to 'authentication' for CI3 Encryption compatibility.
*/
public string $authKeyInfo = '';
/**
* Cipher to use.
* This setting is only used by OpenSSLHandler.
*
* Set to 'AES-128-CBC' to decrypt encrypted data that encrypted
* by CI3 Encryption default configuration.
*/
public string $cipher = 'AES-256-CTR';
}

55
app/Config/Events.php Executable file
View File

@ -0,0 +1,55 @@
<?php
namespace Config;
use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\FrameworkException;
use CodeIgniter\HotReloader\HotReloader;
/*
* --------------------------------------------------------------------
* Application Events
* --------------------------------------------------------------------
* Events allow you to tap into the execution of the program without
* modifying or extending core files. This file provides a central
* location to define your events, though they can always be added
* at run-time, also, if needed.
*
* You create code that can execute by subscribing to events with
* the 'on()' method. This accepts any form of callable, including
* Closures, that will be executed when the event is triggered.
*
* Example:
* Events::on('create', [$myInstance, 'myMethod']);
*/
Events::on('pre_system', static function () {
if (ENVIRONMENT !== 'testing') {
if (ini_get('zlib.output_compression')) {
throw FrameworkException::forEnabledZlibOutputCompression();
}
while (ob_get_level() > 0) {
ob_end_flush();
}
ob_start(static fn ($buffer) => $buffer);
}
/*
* --------------------------------------------------------------------
* Debug Toolbar Listeners.
* --------------------------------------------------------------------
* If you delete, they will no longer be collected.
*/
if (CI_DEBUG && ! is_cli()) {
Events::on('DBQuery', 'CodeIgniter\Debug\Toolbar\Collectors\Database::collect');
Services::toolbar()->respond();
// Hot Reload route - for framework use on the hot reloader.
if (ENVIRONMENT === 'development') {
Services::routes()->get('__hot-reload', static function () {
(new HotReloader())->run();
});
}
}
});

104
app/Config/Exceptions.php Executable file
View File

@ -0,0 +1,104 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
use App\Debug\CorsExceptionHandler;
use CodeIgniter\Debug\ExceptionHandlerInterface;
use Psr\Log\LogLevel;
use Throwable;
/**
* Setup how the exception handler works.
*/
class Exceptions extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* LOG EXCEPTIONS?
* --------------------------------------------------------------------------
* If true, then exceptions will be logged
* through Services::Log.
*
* Default: true
*/
public bool $log = true;
/**
* --------------------------------------------------------------------------
* DO NOT LOG STATUS CODES
* --------------------------------------------------------------------------
* Any status codes here will NOT be logged if logging is turned on.
* By default, only 404 (Page Not Found) exceptions are ignored.
*/
public array $ignoreCodes = [404];
/**
* --------------------------------------------------------------------------
* Error Views Path
* --------------------------------------------------------------------------
* This is the path to the directory that contains the 'cli' and 'html'
* directories that hold the views used to generate errors.
*
* Default: APPPATH.'Views/errors'
*/
public string $errorViewPath = APPPATH . 'Views/errors';
/**
* --------------------------------------------------------------------------
* HIDE FROM DEBUG TRACE
* --------------------------------------------------------------------------
* Any data that you would like to hide from the debug trace.
* In order to specify 2 levels, use "/" to separate.
* ex. ['server', 'setup/password', 'secret_token']
*/
public array $sensitiveDataInTrace = [];
/**
* --------------------------------------------------------------------------
* LOG DEPRECATIONS INSTEAD OF THROWING?
* --------------------------------------------------------------------------
* By default, CodeIgniter converts deprecations into exceptions. Also,
* starting in PHP 8.1 will cause a lot of deprecated usage warnings.
* Use this option to temporarily cease the warnings and instead log those.
* This option also works for user deprecations.
*/
public bool $logDeprecations = true;
/**
* --------------------------------------------------------------------------
* LOG LEVEL THRESHOLD FOR DEPRECATIONS
* --------------------------------------------------------------------------
* If `$logDeprecations` is set to `true`, this sets the log level
* to which the deprecation will be logged. This should be one of the log
* levels recognized by PSR-3.
*
* The related `Config\Logger::$threshold` should be adjusted, if needed,
* to capture logging the deprecations.
*/
public string $deprecationLogLevel = LogLevel::WARNING;
/*
* DEFINE THE HANDLERS USED
* --------------------------------------------------------------------------
* Given the HTTP status code, returns exception handler that
* should be used to deal with this error. By default, it will run CodeIgniter's
* default handler and display the error information in the expected format
* for CLI, HTTP, or AJAX requests, as determined by is_cli() and the expected
* response format.
*
* Custom handlers can be returned if you want to handle one or more specific
* error codes yourself like:
*
* if (in_array($statusCode, [400, 404, 500])) {
* return new \App\Libraries\MyExceptionHandler();
* }
* if ($exception instanceOf PageNotFoundException) {
* return new \App\Libraries\MyExceptionHandler();
* }
*/
public function handler(int $statusCode, Throwable $exception): ExceptionHandlerInterface
{
return new CorsExceptionHandler($this);
}
}

30
app/Config/Feature.php Executable file
View File

@ -0,0 +1,30 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
/**
* Enable/disable backward compatibility breaking features.
*/
class Feature extends BaseConfig
{
/**
* Enable multiple filters for a route or not.
*
* If you enable this:
* - CodeIgniter\CodeIgniter::handleRequest() uses:
* - CodeIgniter\Filters\Filters::enableFilters(), instead of enableFilter()
* - CodeIgniter\CodeIgniter::tryToRouteIt() uses:
* - CodeIgniter\Router\Router::getFilters(), instead of getFilter()
* - CodeIgniter\Router\Router::handle() uses:
* - property $filtersInfo, instead of $filterInfo
* - CodeIgniter\Router\RouteCollection::getFiltersForRoute(), instead of getFilterForRoute()
*/
public bool $multipleFilters = true;
/**
* Use improved new auto routing instead of the default legacy version.
*/
public bool $autoRoutesImproved = false;
}

106
app/Config/Filters.php Executable file
View File

@ -0,0 +1,106 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Filters\CSRF;
use CodeIgniter\Filters\DebugToolbar;
use CodeIgniter\Filters\Honeypot;
use CodeIgniter\Filters\InvalidChars;
use CodeIgniter\Filters\SecureHeaders;
use App\Filters\AuthMVC;
use App\Filters\HttpRequestLog;
use App\Filters\CloseDbConnection;
use App\Filters\AuthClientApi;
use App\Filters\CommissionApiFilter;
use App\Filters\VerifyAppSignature;
use App\Filters\Cors;
use App\Filters\SecurityInputFilter;
use App\Filters\GlobalPostFileUploadGuard;
use App\Filters\AclFilter;
use App\Filters\RateLimitFilter;
use App\Filters\JwtApiRateLimitFilter;
use App\Filters\AuthApiRateLimitFilter;
use App\Filters\AuthJWT;
class Filters extends BaseConfig
{
/**
* Configures aliases for Filter classes to
* make reading things nicer and simpler.
*
* @var array<string, array<int, string>|string> [filter_name => classname]
* or [filter_name => [classname1, classname2, ...]]
* @phpstan-var array<string, class-string|list<class-string>>
*/
public array $aliases = [
'csrf' => CSRF::class,
'toolbar' => DebugToolbar::class,
'honeypot' => Honeypot::class,
'invalidchars' => InvalidChars::class,
'secureheaders' => SecureHeaders::class,
'authMVC' => AuthMVC::class,
'HttpRequestLog' => HttpRequestLog::class,
'authJWT' => AuthJWT::class,
'AuthClientApi' => AuthClientApi::class,
'CloseDbConnection' => CloseDbConnection::class,
'CommissionApiFilter'=> CommissionApiFilter::class,
'appSignature' => VerifyAppSignature::class,
'Cors' => Cors::class,
'SecurityInputFilter' => SecurityInputFilter::class,
'GlobalPostFileUploadGuard' => GlobalPostFileUploadGuard::class,
'AclFilter' => AclFilter::class,
'ratelimit' => RateLimitFilter::class,
'AuthApiRateLimitFilter' => AuthApiRateLimitFilter::class,
'JwtApiRateLimitFilter' => JwtApiRateLimitFilter::class,
];
/**
* List of filter aliases that are always
* applied before and after every request.
*
* @var array<string, array<string, array<string, string>>>|array<string, array<string>>
* @phpstan-var array<string, list<string>>|array<string, array<string, array<string, string>>>
*/
public array $globals = [
'before' => [
'HttpRequestLog' => ['except' => 'cli/*'],
'Cors',
'AclFilter' => ['except' => ['login', 'logout', 'auth/*', 'oauth2callback','claim-form-download', 'claims-feedback-form', 'autobookstackLogin','employeeRest/*','processjob','getCommission','downloadEmployeeEcardZip', 'downloadClaimFile/*', 'api/v1/*', 'cronCdLowBalanceAlert', 'sendCroneRemainderMail']],
'SecurityInputFilter' => ['except' => ['/client/notification/create','/ticket/crud_mail_template/*','test_mail','leads/sendMail', 'ticket/reply', 'policy_tranction/retail-policy/template', 'policy_tranction/retail-policy/send-test-mail'] ],
'GlobalPostFileUploadGuard'
// 'csrf',
// 'invalidchars',
],
'after' => [
'CloseDbConnection',
'Cors',
// 'secureheaders',
],
];
/**
* List of filter aliases that works on a
* particular HTTP method (GET, POST, etc.).
*
* Example:
* 'post' => ['foo', 'bar']
*
* If you use this, you should disable auto-routing because auto-routing
* permits any HTTP method to access a controller. Accessing the controller
* with a method you don't expect could bypass the filter.
*/
public array $methods = [];
/**
* List of filter aliases that should run on any
* before or after URI patterns.
*
* Example:
* 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
*/
// public array $filters = [ 'Cors' => ['before' => ['employeeRest/*']]];
}

View File

@ -0,0 +1,12 @@
<?php
namespace Config;
use CodeIgniter\Config\ForeignCharacters as BaseForeignCharacters;
/**
* @immutable
*/
class ForeignCharacters extends BaseForeignCharacters
{
}

77
app/Config/Format.php Executable file
View File

@ -0,0 +1,77 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Format\FormatterInterface;
use CodeIgniter\Format\JSONFormatter;
use CodeIgniter\Format\XMLFormatter;
class Format extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Available Response Formats
* --------------------------------------------------------------------------
*
* When you perform content negotiation with the request, these are the
* available formats that your application supports. This is currently
* only used with the API\ResponseTrait. A valid Formatter must exist
* for the specified format.
*
* These formats are only checked when the data passed to the respond()
* method is an array.
*
* @var string[]
*/
public array $supportedResponseFormats = [
'application/json',
'application/xml', // machine-readable XML
'text/xml', // human-readable XML
];
/**
* --------------------------------------------------------------------------
* Formatters
* --------------------------------------------------------------------------
*
* Lists the class to use to format responses with of a particular type.
* For each mime type, list the class that should be used. Formatters
* can be retrieved through the getFormatter() method.
*
* @var array<string, string>
*/
public array $formatters = [
'application/json' => JSONFormatter::class,
'application/xml' => XMLFormatter::class,
'text/xml' => XMLFormatter::class,
];
/**
* --------------------------------------------------------------------------
* Formatters Options
* --------------------------------------------------------------------------
*
* Additional Options to adjust default formatters behaviour.
* For each mime type, list the additional options that should be used.
*
* @var array<string, int>
*/
public array $formatterOptions = [
'application/json' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
'application/xml' => 0,
'text/xml' => 0,
];
/**
* A Factory method to return the appropriate formatter for the given mime type.
*
* @return FormatterInterface
*
* @deprecated This is an alias of `\CodeIgniter\Format\Format::getFormatter`. Use that instead.
*/
public function getFormatter(string $mime)
{
return Services::format()->getFormatter($mime);
}
}

42
app/Config/Generators.php Executable file
View File

@ -0,0 +1,42 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class Generators extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Generator Commands' Views
* --------------------------------------------------------------------------
*
* This array defines the mapping of generator commands to the view files
* they are using. If you need to customize them for your own, copy these
* view files in your own folder and indicate the location here.
*
* You will notice that the views have special placeholders enclosed in
* curly braces `{...}`. These placeholders are used internally by the
* generator commands in processing replacements, thus you are warned
* not to delete them or modify the names. If you will do so, you may
* end up disrupting the scaffolding process and throw errors.
*
* YOU HAVE BEEN WARNED!
*
* @var array<string, string>
*/
public array $views = [
'make:cell' => 'CodeIgniter\Commands\Generators\Views\cell.tpl.php',
'make:cell_view' => 'CodeIgniter\Commands\Generators\Views\cell_view.tpl.php',
'make:command' => 'CodeIgniter\Commands\Generators\Views\command.tpl.php',
'make:config' => 'CodeIgniter\Commands\Generators\Views\config.tpl.php',
'make:controller' => 'CodeIgniter\Commands\Generators\Views\controller.tpl.php',
'make:entity' => 'CodeIgniter\Commands\Generators\Views\entity.tpl.php',
'make:filter' => 'CodeIgniter\Commands\Generators\Views\filter.tpl.php',
'make:migration' => 'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
'make:model' => 'CodeIgniter\Commands\Generators\Views\model.tpl.php',
'make:seeder' => 'CodeIgniter\Commands\Generators\Views\seeder.tpl.php',
'make:validation' => 'CodeIgniter\Commands\Generators\Views\validation.tpl.php',
'session:migration' => 'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
];
}

42
app/Config/Honeypot.php Executable file
View File

@ -0,0 +1,42 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class Honeypot extends BaseConfig
{
/**
* Makes Honeypot visible or not to human
*/
public bool $hidden = true;
/**
* Honeypot Label Content
*/
public string $label = 'Fill This Field';
/**
* Honeypot Field Name
*/
public string $name = 'honeypot';
/**
* Honeypot HTML Template
*/
public string $template = '<label>{label}</label><input type="text" name="{name}" value="">';
/**
* Honeypot container
*
* If you enabled CSP, you can remove `style="display:none"`.
*/
public string $container = '<div style="display:none">{template}</div>';
/**
* The id attribute for Honeypot container tag
*
* Used when CSP is enabled.
*/
public string $containerId = 'hpc';
}

View File

@ -0,0 +1,18 @@
<?php
/**
* HR Login Diagnosis routes (loaded via Config\Routing::$routeFiles).
*
* @var \CodeIgniter\Router\RouteCollection $routes
*/
$routes->group('hrLoginDiagnose', ['filter' => 'authMVC'], static function ($routes) {
$routes->get('/', 'HrLoginDiagnoseController::index');
$routes->get('', 'HrLoginDiagnoseController::index');
$routes->post('check', 'HrLoginDiagnoseController::check');
});
// Fallback when request path still includes index.php (wrong baseURL / form action).
$routes->get('index.php/hrLoginDiagnose', 'HrLoginDiagnoseController::index', ['filter' => 'authMVC']);
$routes->get('index.php/hrLoginDiagnose/', 'HrLoginDiagnoseController::index', ['filter' => 'authMVC']);
$routes->post('index.php/hrLoginDiagnose/check', 'HrLoginDiagnoseController::check', ['filter' => 'authMVC']);

31
app/Config/Images.php Executable file
View File

@ -0,0 +1,31 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Images\Handlers\GDHandler;
use CodeIgniter\Images\Handlers\ImageMagickHandler;
class Images extends BaseConfig
{
/**
* Default handler used if no other handler is specified.
*/
public string $defaultHandler = 'gd';
/**
* The path to the image library.
* Required for ImageMagick, GraphicsMagick, or NetPBM.
*/
public string $libraryPath = '/usr/local/bin/convert';
/**
* The available handler classes.
*
* @var array<string, string>
*/
public array $handlers = [
'gd' => GDHandler::class,
'imagick' => ImageMagickHandler::class,
];
}

66
app/Config/Kint.php Executable file
View File

@ -0,0 +1,66 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
use Kint\Parser\ConstructablePluginInterface;
use Kint\Renderer\AbstractRenderer;
use Kint\Renderer\Rich\TabPluginInterface;
use Kint\Renderer\Rich\ValuePluginInterface;
/**
* --------------------------------------------------------------------------
* Kint
* --------------------------------------------------------------------------
*
* We use Kint's `RichRenderer` and `CLIRenderer`. This area contains options
* that you can set to customize how Kint works for you.
*
* @see https://kint-php.github.io/kint/ for details on these settings.
*/
class Kint extends BaseConfig
{
/*
|--------------------------------------------------------------------------
| Global Settings
|--------------------------------------------------------------------------
*/
/**
* @var list<class-string<ConstructablePluginInterface>|ConstructablePluginInterface>|null
*/
public $plugins;
public int $maxDepth = 6;
public bool $displayCalledFrom = true;
public bool $expanded = false;
/*
|--------------------------------------------------------------------------
| RichRenderer Settings
|--------------------------------------------------------------------------
*/
public string $richTheme = 'aante-light.css';
public bool $richFolder = false;
public int $richSort = AbstractRenderer::SORT_FULL;
/**
* @var array<string, class-string<ValuePluginInterface>>|null
*/
public $richObjectPlugins;
/**
* @var array<string, class-string<TabPluginInterface>>|null
*/
public $richTabPlugins;
/*
|--------------------------------------------------------------------------
| CLI Settings
|--------------------------------------------------------------------------
*/
public bool $cliColors = true;
public bool $cliForceUTF8 = false;
public bool $cliDetectWidth = true;
public int $cliMinWidth = 40;
}

148
app/Config/Logger.php Executable file
View File

@ -0,0 +1,148 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Log\Handlers\FileHandler;
class Logger extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Error Logging Threshold
* --------------------------------------------------------------------------
*
* You can enable error logging by setting a threshold over zero. The
* threshold determines what gets logged. Any values below or equal to the
* threshold will be logged.
*
* Threshold options are:
*
* - 0 = Disables logging, Error logging TURNED OFF
* - 1 = Emergency Messages - System is unusable
* - 2 = Alert Messages - Action Must Be Taken Immediately
* - 3 = Critical Messages - Application component unavailable, unexpected exception.
* - 4 = Runtime Errors - Don't need immediate action, but should be monitored.
* - 5 = Warnings - Exceptional occurrences that are not errors.
* - 6 = Notices - Normal but significant events.
* - 7 = Info - Interesting events, like user logging in, etc.
* - 8 = Debug - Detailed debug information.
* - 9 = All Messages
*
* You can also pass an array with threshold levels to show individual error types
*
* array(1, 2, 3, 8) = Emergency, Alert, Critical, and Debug messages
*
* For a live site you'll usually enable Critical or higher (3) to be logged otherwise
* your log files will fill up very fast.
*
* @var array|int
*/
public $threshold = (ENVIRONMENT === 'production') ? 4 : 9;
/**
* --------------------------------------------------------------------------
* Date Format for Logs
* --------------------------------------------------------------------------
*
* Each item that is logged has an associated date. You can use PHP date
* codes to set your own date formatting
*/
public string $dateFormat = 'Y-m-d H:i:s';
/**
* --------------------------------------------------------------------------
* Log Handlers
* --------------------------------------------------------------------------
*
* The logging system supports multiple actions to be taken when something
* is logged. This is done by allowing for multiple Handlers, special classes
* designed to write the log to their chosen destinations, whether that is
* a file on the getServer, a cloud-based service, or even taking actions such
* as emailing the dev team.
*
* Each handler is defined by the class name used for that handler, and it
* MUST implement the `CodeIgniter\Log\Handlers\HandlerInterface` interface.
*
* The value of each key is an array of configuration items that are sent
* to the constructor of each handler. The only required configuration item
* is the 'handles' element, which must be an array of integer log levels.
* This is most easily handled by using the constants defined in the
* `Psr\Log\LogLevel` class.
*
* Handlers are executed in the order defined in this array, starting with
* the handler on top and continuing down.
*/
public array $handlers = [
/*
* --------------------------------------------------------------------
* File Handler
* --------------------------------------------------------------------
*/
FileHandler::class => [
// The log levels that this handler will handle.
'handles' => [
'critical',
'alert',
'emergency',
'debug',
'error',
'info',
'notice',
'warning',
],
/*
* The default filename extension for log files.
* An extension of 'php' allows for protecting the log files via basic
* scripting, when they are to be stored under a publicly accessible directory.
*
* NOTE: Leaving it blank will default to 'log'.
*/
'fileExtension' => '',
/*
* The file system permissions to be applied on newly created log files.
*
* IMPORTANT: This MUST be an integer (no quotes) and you MUST use octal
* integer notation (i.e. 0700, 0644, etc.)
*/
'filePermissions' => 0644,
/*
* Logging Directory Path
*
* By default, logs are written to WRITEPATH . 'logs/'
* Specify a different destination here, if desired.
*/
'path' => '',
],
/*
* The ChromeLoggerHandler requires the use of the Chrome web browser
* and the ChromeLogger extension. Uncomment this block to use it.
*/
// 'CodeIgniter\Log\Handlers\ChromeLoggerHandler' => [
// /*
// * The log levels that this handler will handle.
// */
// 'handles' => ['critical', 'alert', 'emergency', 'debug',
// 'error', 'info', 'notice', 'warning'],
// ],
/*
* The ErrorlogHandler writes the logs to PHP's native `error_log()` function.
* Uncomment this block to use it.
*/
// 'CodeIgniter\Log\Handlers\ErrorlogHandler' => [
// /* The log levels this handler can handle. */
// 'handles' => ['critical', 'alert', 'emergency', 'debug', 'error', 'info', 'notice', 'warning'],
//
// /*
// * The message type where the error should go. Can be 0 or 4, or use the
// * class constants: `ErrorlogHandler::TYPE_OS` (0) or `ErrorlogHandler::TYPE_SAPI` (4)
// */
// 'messageType' => 0,
// ],
];
}

50
app/Config/Migrations.php Executable file
View File

@ -0,0 +1,50 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class Migrations extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Enable/Disable Migrations
* --------------------------------------------------------------------------
*
* Migrations are enabled by default.
*
* You should enable migrations whenever you intend to do a schema migration
* and disable it back when you're done.
*/
public bool $enabled = true;
/**
* --------------------------------------------------------------------------
* Migrations Table
* --------------------------------------------------------------------------
*
* This is the name of the table that will store the current migrations state.
* When migrations runs it will store in a database table which migration
* files have already been run.
*/
public string $table = 'migrations';
/**
* --------------------------------------------------------------------------
* Timestamp Format
* --------------------------------------------------------------------------
*
* This is the format that will be used when creating new migrations
* using the CLI command:
* > php spark make:migration
*
* NOTE: if you set an unsupported format, migration runner will not find
* your migration files.
*
* Supported formats:
* - YmdHis_
* - Y-m-d-His_
* - Y_m_d_His_
*/
public string $timestampFormat = 'Y-m-d-His_';
}

534
app/Config/Mimes.php Executable file
View File

@ -0,0 +1,534 @@
<?php
namespace Config;
/**
* Mimes
*
* This file contains an array of mime types. It is used by the
* Upload class to help identify allowed file types.
*
* When more than one variation for an extension exist (like jpg, jpeg, etc)
* the most common one should be first in the array to aid the guess*
* methods. The same applies when more than one mime-type exists for a
* single extension.
*
* When working with mime types, please make sure you have the ´fileinfo´
* extension enabled to reliably detect the media types.
*
* @immutable
*/
class Mimes
{
/**
* Map of extensions to mime types.
*/
public static array $mimes = [
'hqx' => [
'application/mac-binhex40',
'application/mac-binhex',
'application/x-binhex40',
'application/x-mac-binhex40',
],
'cpt' => 'application/mac-compactpro',
'csv' => [
'text/csv',
'text/x-comma-separated-values',
'text/comma-separated-values',
'application/vnd.ms-excel',
'application/x-csv',
'text/x-csv',
'application/csv',
'application/excel',
'application/vnd.msexcel',
'text/plain',
],
'bin' => [
'application/macbinary',
'application/mac-binary',
'application/octet-stream',
'application/x-binary',
'application/x-macbinary',
],
'dms' => 'application/octet-stream',
'lha' => 'application/octet-stream',
'lzh' => 'application/octet-stream',
'exe' => [
'application/octet-stream',
'application/vnd.microsoft.portable-executable',
'application/x-dosexec',
'application/x-msdownload',
],
'class' => 'application/octet-stream',
'psd' => [
'application/x-photoshop',
'image/vnd.adobe.photoshop',
],
'so' => 'application/octet-stream',
'sea' => 'application/octet-stream',
'dll' => 'application/octet-stream',
'oda' => 'application/oda',
'pdf' => [
'application/pdf',
'application/force-download',
'application/x-download',
],
'ai' => [
'application/pdf',
'application/postscript',
],
'eps' => 'application/postscript',
'ps' => 'application/postscript',
'smi' => 'application/smil',
'smil' => 'application/smil',
'mif' => 'application/vnd.mif',
'xls' => [
'application/vnd.ms-excel',
'application/msexcel',
'application/x-msexcel',
'application/x-ms-excel',
'application/x-excel',
'application/x-dos_ms_excel',
'application/xls',
'application/x-xls',
'application/excel',
'application/download',
'application/vnd.ms-office',
'application/msword',
],
'ppt' => [
'application/vnd.ms-powerpoint',
'application/powerpoint',
'application/vnd.ms-office',
'application/msword',
],
'pptx' => [
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
],
'wbxml' => 'application/wbxml',
'wmlc' => 'application/wmlc',
'dcr' => 'application/x-director',
'dir' => 'application/x-director',
'dxr' => 'application/x-director',
'dvi' => 'application/x-dvi',
'gtar' => 'application/x-gtar',
'gz' => 'application/x-gzip',
'gzip' => 'application/x-gzip',
'php' => [
'application/x-php',
'application/x-httpd-php',
'application/php',
'text/php',
'text/x-php',
'application/x-httpd-php-source',
],
'php4' => 'application/x-httpd-php',
'php3' => 'application/x-httpd-php',
'phtml' => 'application/x-httpd-php',
'phps' => 'application/x-httpd-php-source',
'js' => [
'application/x-javascript',
'text/plain',
],
'swf' => 'application/x-shockwave-flash',
'sit' => 'application/x-stuffit',
'tar' => 'application/x-tar',
'tgz' => [
'application/x-tar',
'application/x-gzip-compressed',
],
'z' => 'application/x-compress',
'xhtml' => 'application/xhtml+xml',
'xht' => 'application/xhtml+xml',
'zip' => [
'application/x-zip',
'application/zip',
'application/x-zip-compressed',
'application/s-compressed',
'multipart/x-zip',
],
'rar' => [
'application/vnd.rar',
'application/x-rar',
'application/rar',
'application/x-rar-compressed',
],
'mid' => 'audio/midi',
'midi' => 'audio/midi',
'mpga' => 'audio/mpeg',
'mp2' => 'audio/mpeg',
'mp3' => [
'audio/mpeg',
'audio/mpg',
'audio/mpeg3',
'audio/mp3',
],
'aif' => [
'audio/x-aiff',
'audio/aiff',
],
'aiff' => [
'audio/x-aiff',
'audio/aiff',
],
'aifc' => 'audio/x-aiff',
'ram' => 'audio/x-pn-realaudio',
'rm' => 'audio/x-pn-realaudio',
'rpm' => 'audio/x-pn-realaudio-plugin',
'ra' => 'audio/x-realaudio',
'rv' => 'video/vnd.rn-realvideo',
'wav' => [
'audio/x-wav',
'audio/wave',
'audio/wav',
],
'bmp' => [
'image/bmp',
'image/x-bmp',
'image/x-bitmap',
'image/x-xbitmap',
'image/x-win-bitmap',
'image/x-windows-bmp',
'image/ms-bmp',
'image/x-ms-bmp',
'application/bmp',
'application/x-bmp',
'application/x-win-bitmap',
],
'gif' => 'image/gif',
'jpg' => [
'image/jpeg',
'image/pjpeg',
],
'jpeg' => [
'image/jpeg',
'image/pjpeg',
],
'jpe' => [
'image/jpeg',
'image/pjpeg',
],
'jp2' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'j2k' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'jpf' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'jpg2' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'jpx' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'jpm' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'mj2' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'mjp2' => [
'image/jp2',
'video/mj2',
'image/jpx',
'image/jpm',
],
'png' => [
'image/png',
'image/x-png',
],
'webp' => 'image/webp',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'css' => [
'text/css',
'text/plain',
],
'html' => [
'text/html',
'text/plain',
],
'htm' => [
'text/html',
'text/plain',
],
'shtml' => [
'text/html',
'text/plain',
],
'txt' => 'text/plain',
'text' => 'text/plain',
'log' => [
'text/plain',
'text/x-log',
],
'rtx' => 'text/richtext',
'rtf' => 'text/rtf',
'xml' => [
'application/xml',
'text/xml',
'text/plain',
],
'xsl' => [
'application/xml',
'text/xsl',
'text/xml',
],
'mpeg' => 'video/mpeg',
'mpg' => 'video/mpeg',
'mpe' => 'video/mpeg',
'qt' => 'video/quicktime',
'mov' => 'video/quicktime',
'avi' => [
'video/x-msvideo',
'video/msvideo',
'video/avi',
'application/x-troff-msvideo',
],
'movie' => 'video/x-sgi-movie',
'doc' => [
'application/msword',
'application/vnd.ms-office',
],
'docx' => [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/zip',
'application/msword',
'application/x-zip',
],
'dot' => [
'application/msword',
'application/vnd.ms-office',
],
'dotx' => [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/zip',
'application/msword',
],
'xlsx' => [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/zip',
'application/vnd.ms-excel',
'application/msword',
'application/x-zip',
],
'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
'word' => [
'application/msword',
'application/octet-stream',
],
'xl' => 'application/excel',
'eml' => 'message/rfc822',
'json' => [
'application/json',
'text/json',
],
'pem' => [
'application/x-x509-user-cert',
'application/x-pem-file',
'application/octet-stream',
],
'p10' => [
'application/x-pkcs10',
'application/pkcs10',
],
'p12' => 'application/x-pkcs12',
'p7a' => 'application/x-pkcs7-signature',
'p7c' => [
'application/pkcs7-mime',
'application/x-pkcs7-mime',
],
'p7m' => [
'application/pkcs7-mime',
'application/x-pkcs7-mime',
],
'p7r' => 'application/x-pkcs7-certreqresp',
'p7s' => 'application/pkcs7-signature',
'crt' => [
'application/x-x509-ca-cert',
'application/x-x509-user-cert',
'application/pkix-cert',
],
'crl' => [
'application/pkix-crl',
'application/pkcs-crl',
],
'der' => 'application/x-x509-ca-cert',
'kdb' => 'application/octet-stream',
'pgp' => 'application/pgp',
'gpg' => 'application/gpg-keys',
'sst' => 'application/octet-stream',
'csr' => 'application/octet-stream',
'rsa' => 'application/x-pkcs7',
'cer' => [
'application/pkix-cert',
'application/x-x509-ca-cert',
],
'3g2' => 'video/3gpp2',
'3gp' => [
'video/3gp',
'video/3gpp',
],
'mp4' => 'video/mp4',
'm4a' => 'audio/x-m4a',
'f4v' => [
'video/mp4',
'video/x-f4v',
],
'flv' => 'video/x-flv',
'webm' => 'video/webm',
'aac' => 'audio/x-acc',
'm4u' => 'application/vnd.mpegurl',
'm3u' => 'text/plain',
'xspf' => 'application/xspf+xml',
'vlc' => 'application/videolan',
'wmv' => [
'video/x-ms-wmv',
'video/x-ms-asf',
],
'au' => 'audio/x-au',
'ac3' => 'audio/ac3',
'flac' => 'audio/x-flac',
'ogg' => [
'audio/ogg',
'video/ogg',
'application/ogg',
],
'kmz' => [
'application/vnd.google-earth.kmz',
'application/zip',
'application/x-zip',
],
'kml' => [
'application/vnd.google-earth.kml+xml',
'application/xml',
'text/xml',
],
'ics' => 'text/calendar',
'ical' => 'text/calendar',
'zsh' => 'text/x-scriptzsh',
'7zip' => [
'application/x-compressed',
'application/x-zip-compressed',
'application/zip',
'multipart/x-zip',
],
'cdr' => [
'application/cdr',
'application/coreldraw',
'application/x-cdr',
'application/x-coreldraw',
'image/cdr',
'image/x-cdr',
'zz-application/zz-winassoc-cdr',
],
'wma' => [
'audio/x-ms-wma',
'video/x-ms-asf',
],
'jar' => [
'application/java-archive',
'application/x-java-application',
'application/x-jar',
'application/x-compressed',
],
'svg' => [
'image/svg+xml',
'image/svg',
'application/xml',
'text/xml',
],
'vcf' => 'text/x-vcard',
'srt' => [
'text/srt',
'text/plain',
],
'vtt' => [
'text/vtt',
'text/plain',
],
'ico' => [
'image/x-icon',
'image/x-ico',
'image/vnd.microsoft.icon',
],
'stl' => [
'application/sla',
'application/vnd.ms-pki.stl',
'application/x-navistyle',
],
];
/**
* Attempts to determine the best mime type for the given file extension.
*
* @return string|null The mime type found, or none if unable to determine.
*/
public static function guessTypeFromExtension(string $extension)
{
$extension = trim(strtolower($extension), '. ');
if (! array_key_exists($extension, static::$mimes)) {
return null;
}
return is_array(static::$mimes[$extension]) ? static::$mimes[$extension][0] : static::$mimes[$extension];
}
/**
* Attempts to determine the best file extension for a given mime type.
*
* @param string|null $proposedExtension - default extension (in case there is more than one with the same mime type)
*
* @return string|null The extension determined, or null if unable to match.
*/
public static function guessExtensionFromType(string $type, ?string $proposedExtension = null)
{
$type = trim(strtolower($type), '. ');
$proposedExtension = trim(strtolower($proposedExtension ?? ''));
if (
$proposedExtension !== ''
&& array_key_exists($proposedExtension, static::$mimes)
&& in_array($type, (array) static::$mimes[$proposedExtension], true)
) {
// The detected mime type matches with the proposed extension.
return $proposedExtension;
}
// Reverse check the mime type list if no extension was proposed.
// This search is order sensitive!
foreach (static::$mimes as $ext => $types) {
if (in_array($type, (array) $types, true)) {
return $ext;
}
}
return null;
}
}

84
app/Config/Modules.php Executable file
View File

@ -0,0 +1,84 @@
<?php
namespace Config;
use CodeIgniter\Modules\Modules as BaseModules;
/**
* Modules Configuration.
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*
* @immutable
*/
class Modules extends BaseModules
{
/**
* --------------------------------------------------------------------------
* Enable Auto-Discovery?
* --------------------------------------------------------------------------
*
* If true, then auto-discovery will happen across all elements listed in
* $aliases below. If false, no auto-discovery will happen at all,
* giving a slight performance boost.
*
* @var bool
*/
public $enabled = true;
/**
* --------------------------------------------------------------------------
* Enable Auto-Discovery Within Composer Packages?
* --------------------------------------------------------------------------
*
* If true, then auto-discovery will happen across all namespaces loaded
* by Composer, as well as the namespaces configured locally.
*
* @var bool
*/
public $discoverInComposer = true;
/**
* The Composer package list for Auto-Discovery
* This setting is optional.
*
* E.g.:
* [
* 'only' => [
* // List up all packages to auto-discover
* 'codeigniter4/shield',
* ],
* ]
* or
* [
* 'exclude' => [
* // List up packages to exclude.
* 'pestphp/pest',
* ],
* ]
*
* @var array{only?: list<string>, exclude?: list<string>}
*/
public $composerPackages = [];
/**
* --------------------------------------------------------------------------
* Auto-Discovery Rules
* --------------------------------------------------------------------------
*
* Aliases list of all discovery classes that will be active and used during
* the current application request.
*
* If it is not listed, only the base application elements will be used.
*
* @var list<string>
*/
public $aliases = [
'events',
'filters',
'registrars',
'routes',
'services',
];
}

37
app/Config/Pager.php Executable file
View File

@ -0,0 +1,37 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class Pager extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Templates
* --------------------------------------------------------------------------
*
* Pagination links are rendered out using views to configure their
* appearance. This array contains aliases and the view names to
* use when rendering the links.
*
* Within each view, the Pager object will be available as $pager,
* and the desired group as $pagerGroup;
*
* @var array<string, string>
*/
public array $templates = [
'default_full' => 'CodeIgniter\Pager\Views\default_full',
'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
'default_head' => 'CodeIgniter\Pager\Views\default_head',
];
/**
* --------------------------------------------------------------------------
* Items Per Page
* --------------------------------------------------------------------------
*
* The default number of results shown in a single page.
*/
public int $perPage = 20;
}

80
app/Config/Paths.php Executable file
View File

@ -0,0 +1,80 @@
<?php
namespace Config;
/**
* Paths
*
* Holds the paths that are used by the system to
* locate the main directories, app, system, etc.
*
* Modifying these allows you to restructure your application,
* share a system folder between multiple applications, and more.
*
* All paths are relative to the project's root folder.
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*
* @immutable
*/
class Paths
{
/**
* ---------------------------------------------------------------
* SYSTEM FOLDER NAME
* ---------------------------------------------------------------
*
* This must contain the name of your "system" folder. Include
* the path if the folder is not in the same directory as this file.
*/
public string $systemDirectory = __DIR__ . '/../../system';
/**
* ---------------------------------------------------------------
* APPLICATION FOLDER NAME
* ---------------------------------------------------------------
*
* If you want this front controller to use a different "app"
* folder than the default one you can set its name here. The folder
* can also be renamed or relocated anywhere on your server. If
* you do, use a full server path.
*
* @see http://codeigniter.com/user_guide/general/managing_apps.html
*/
public string $appDirectory = __DIR__ . '/..';
/**
* ---------------------------------------------------------------
* WRITABLE DIRECTORY NAME
* ---------------------------------------------------------------
*
* This variable must contain the name of your "writable" directory.
* The writable directory allows you to group all directories that
* need write permission to a single place that can be tucked away
* for maximum security, keeping it out of the app and/or
* system directories.
*/
public string $writableDirectory = __DIR__ . '/../../writable';
/**
* ---------------------------------------------------------------
* TESTS DIRECTORY NAME
* ---------------------------------------------------------------
*
* This variable must contain the name of your "tests" directory.
*/
public string $testsDirectory = __DIR__ . '/../../tests';
/**
* ---------------------------------------------------------------
* VIEW DIRECTORY NAME
* ---------------------------------------------------------------
*
* This variable must contain the name of the directory that
* contains the view files used by your application. By
* default this is in `app/Views`. This value
* is used when no value is provided to `Services::renderer()`.
*/
public string $viewDirectory = __DIR__ . '/../Views';
}

View File

@ -0,0 +1,387 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
use Config\Database;
class PtRenewalReminderConfig extends BaseConfig
{
public bool $enabled = false;
public bool $daily = false;
/** @var string[] Lowercase three-letter day abbreviations, e.g. mon, tue */
public array $days = [];
/** @var int[] Client type ids: 1=Group, 2=Individual */
public array $clientTypes = [];
/** Whitelisted: renewal_date | policy_end_date */
public string $dateField = 'renewal_date';
/** @var int[] Reminder offsets: positive = days before due, negative = days after due */
public array $daysBeforeList = [30];
public bool $includeClientEmail = false;
/** When true, also include policies whose dateField is already past */
public bool $includeOverdue = false;
/** @var string[] */
public array $toEmails = [];
/** @var string[] */
public array $ccEmails = [];
/** @var string[] */
public array $bccEmails = [];
public string $fromMail = '';
/** Loaded from notifications.subject when present */
public string $mailSubject = '';
/** Loaded from notifications.mail_content when present */
public string $mailContent = '';
public ?int $notificationId = null;
private const ALLOWED_DATE_FIELDS = ['renewal_date', 'policy_end_date'];
public const TEMPLATE_NAME = 'retail_reminder_mail';
public function __construct()
{
parent::__construct();
// Env defaults (seed / fallback)
$defaults = [
'enabled' => env('pt.renewalReminder.enabled', 'true'),
'daily' => env('pt.renewalReminder.daily', 'true'),
'days' => env('pt.renewalReminder.days', 'mon,tue,wed,thu,fri'),
'clientTypes' => env('pt.renewalReminder.clientTypes', '2'),
'dateField' => env('pt.renewalReminder.dateField', 'renewal_date'),
'daysBefore' => env('pt.renewalReminder.daysBefore', '30'),
'includeClientEmail' => env('pt.renewalReminder.includeClientEmail', 'true'),
'includeOverdue' => env('pt.renewalReminder.includeOverdue', 'false'),
'toEmails' => env('pt.renewalReminder.toEmails', ''),
'ccEmails' => env('pt.renewalReminder.ccEmails', ''),
'bccEmails' => env('pt.renewalReminder.bccEmails', ''),
'fromMail' => env('pt.renewalReminder.fromMail', ''),
];
$dbConfig = $this->loadConfigFromNotification();
$merged = array_merge($defaults, $dbConfig);
$this->applyMergedConfig($merged);
}
/**
* @return array<string, mixed>
*/
private function loadConfigFromNotification(): array
{
try {
$db = Database::connect();
$row = $db->table('notifications')
->where('template_name', self::TEMPLATE_NAME)
->groupStart()
->where('client_id', null)
->orWhere('client_id', 0)
->orWhere('client_id', '')
->groupEnd()
->orderBy('id', 'DESC')
->get()
->getRowArray();
if (empty($row)) {
log_message('error', '[PtRenewalReminderConfig] No notifications row for retail_reminder_mail; using env defaults');
return [];
}
$this->notificationId = isset($row['id']) ? (int) $row['id'] : null;
$this->mailSubject = trim((string) ($row['subject'] ?? ''));
$this->mailContent = (string) ($row['mail_content'] ?? '');
$json = trim((string) ($row['config_json'] ?? ''));
if ($json === '') {
log_message('error', '[PtRenewalReminderConfig] config_json empty for notification_id=' . ($this->notificationId ?? 'n/a'));
return [];
}
$decoded = json_decode($json, true);
if (! is_array($decoded)) {
log_message('error', '[PtRenewalReminderConfig] Invalid config_json for notification_id=' . ($this->notificationId ?? 'n/a'));
return [];
}
return $decoded;
} catch (\Throwable $e) {
log_message(
'error',
'[PtRenewalReminderConfig] Failed loading notification config: ' . $e->getMessage()
);
return [];
}
}
/**
* @param array<string, mixed> $merged
*/
private function applyMergedConfig(array $merged): void
{
$this->enabled = $this->toBool($merged['enabled'] ?? false);
$this->daily = $this->toBool($merged['daily'] ?? false);
$daysRaw = $merged['days'] ?? 'mon,tue,wed,thu,fri';
if (is_array($daysRaw)) {
$daysRaw = implode(',', $daysRaw);
}
$this->days = array_values(array_filter(array_map(
static fn (string $day): string => strtolower(substr(trim($day), 0, 3)),
explode(',', (string) $daysRaw)
)));
$clientTypesRaw = $merged['clientTypes'] ?? '2';
if (is_array($clientTypesRaw)) {
$this->clientTypes = array_values(array_unique(array_filter(array_map(
static fn ($type): int => (int) $type,
$clientTypesRaw
), static fn (int $type): bool => $type > 0)));
} else {
$this->clientTypes = array_values(array_unique(array_filter(array_map(
static fn (string $type): int => (int) trim($type),
explode(',', (string) $clientTypesRaw)
), static fn (int $type): bool => $type > 0)));
}
$dateField = strtolower(trim((string) ($merged['dateField'] ?? 'renewal_date')));
if (! in_array($dateField, self::ALLOWED_DATE_FIELDS, true)) {
log_message(
'error',
'[PtRenewalReminderConfig] Invalid dateField "' . $dateField . '", falling back to renewal_date'
);
$dateField = 'renewal_date';
}
$this->dateField = $dateField;
$this->daysBeforeList = self::parseDaysBeforeList($merged['daysBefore'] ?? 30);
$this->includeClientEmail = $this->toBool($merged['includeClientEmail'] ?? true);
$this->includeOverdue = $this->toBool($merged['includeOverdue'] ?? false);
$this->toEmails = $this->parseEmailList($merged['toEmails'] ?? '');
$this->ccEmails = $this->parseEmailList($merged['ccEmails'] ?? '');
$this->bccEmails = $this->parseEmailList($merged['bccEmails'] ?? '');
$this->fromMail = trim((string) ($merged['fromMail'] ?? ''));
}
public function shouldRunToday(): bool
{
if (! $this->enabled) {
return false;
}
if ($this->daily) {
return true;
}
$today = strtolower(date('D'));
return in_array($today, $this->days, true);
}
public function getDateField(): string
{
return $this->dateField;
}
/**
* First / primary offset (legacy callers).
*/
public function getDaysBefore(): int
{
return $this->daysBeforeList[0] ?? 30;
}
/**
* @return int[]
*/
public function getDaysBeforeList(): array
{
return $this->daysBeforeList;
}
public function getDaysBeforeDisplay(): string
{
return implode(', ', $this->daysBeforeList);
}
/**
* Map each offset to the calendar date it matches (today + offset).
*
* @return array<int, string> offset => Y-m-d
*/
public function getTargetDates(?string $today = null): array
{
$today = $today ?: date('Y-m-d');
$map = [];
foreach ($this->daysBeforeList as $offset) {
$map[(int) $offset] = date('Y-m-d', strtotime($today . ' ' . sprintf('%+d days', (int) $offset)));
}
return $map;
}
/**
* Parse "30, 15, 7, 1, -2, -5, -10" (or a single int / array) into unique offsets.
*
* @return int[]
*/
public static function parseDaysBeforeList(mixed $value): array
{
if (is_int($value) || is_float($value)) {
$raw = [(int) $value];
} elseif (is_array($value)) {
$raw = $value;
} else {
$raw = preg_split('/[,\s]+/', trim((string) $value), -1, PREG_SPLIT_NO_EMPTY) ?: [];
}
$out = [];
$seen = [];
foreach ($raw as $item) {
$item = trim((string) $item);
if ($item === '' || ! is_numeric($item)) {
continue;
}
$n = (int) $item;
if ($n > 365 || $n < -365 || isset($seen[$n])) {
continue;
}
$seen[$n] = true;
$out[] = $n;
}
return $out !== [] ? $out : [30];
}
/**
* @return int[]
*/
public function getClientTypes(): array
{
return $this->clientTypes;
}
/**
* @return string[]
*/
public function getToEmails(): array
{
return $this->toEmails;
}
/**
* @return string[]
*/
public function getCcEmails(): array
{
return $this->ccEmails;
}
/**
* @return string[]
*/
public function getBccEmails(): array
{
return $this->bccEmails;
}
public function getFromMail(): string
{
return $this->fromMail;
}
public function includeClientEmail(): bool
{
return $this->includeClientEmail;
}
public function includeOverdue(): bool
{
return $this->includeOverdue;
}
public function getMailSubject(): string
{
return $this->mailSubject;
}
public function getMailContent(): string
{
return $this->mailContent;
}
/**
* Flatten config for UI / API response.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'enabled' => $this->enabled,
'daily' => $this->daily,
'days' => implode(',', $this->days),
'clientTypes' => $this->clientTypes,
'dateField' => $this->dateField,
'daysBefore' => $this->getDaysBeforeDisplay(),
'includeClientEmail' => $this->includeClientEmail,
'includeOverdue' => $this->includeOverdue,
'toEmails' => implode(',', $this->toEmails),
'ccEmails' => implode(',', $this->ccEmails),
'bccEmails' => implode(',', $this->bccEmails),
'fromMail' => $this->fromMail,
];
}
/**
* @return string[]
*/
private function parseEmailList(mixed $value): array
{
if ($value === null || $value === '') {
return [];
}
if (is_array($value)) {
$emails = array_map('trim', $value);
} else {
$emails = array_map('trim', explode(',', (string) $value));
}
$unique = [];
foreach ($emails as $email) {
if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
continue;
}
$key = strtolower($email);
if (! isset($unique[$key])) {
$unique[$key] = $email;
}
}
return array_values($unique);
}
private function toBool(mixed $value): bool
{
if (is_bool($value)) {
return $value;
}
return in_array(strtolower((string) $value), ['1', 'true', 'yes', 'on'], true);
}
}

28
app/Config/Publisher.php Executable file
View File

@ -0,0 +1,28 @@
<?php
namespace Config;
use CodeIgniter\Config\Publisher as BasePublisher;
/**
* Publisher Configuration
*
* Defines basic security restrictions for the Publisher class
* to prevent abuse by injecting malicious files into a project.
*/
class Publisher extends BasePublisher
{
/**
* A list of allowed destinations with a (pseudo-)regex
* of allowed files for each destination.
* Attempts to publish to directories not in this list will
* result in a PublisherException. Files that do no fit the
* pattern will cause copy/merge to fail.
*
* @var array<string, string>
*/
public $restrictions = [
ROOTPATH => '*',
FCPATH => '#\.(s?css|js|map|html?|xml|json|webmanifest|ttf|eot|woff2?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i',
];
}

View File

@ -0,0 +1,89 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class RateLimiter extends BaseConfig
{
/*
|--------------------------------------------------------------------------
| JWT / Authenticated API Routes
|--------------------------------------------------------------------------
*/
public array $jwtApi = [
'limit' => 60, // max requests
'window' => 60, // window in seconds
'violation_soft' => 3, // violations before soft block
];
/*
|--------------------------------------------------------------------------
| Auth API Routes (verifyMobile, verifyOTP, etc.)
|--------------------------------------------------------------------------
*/
public array $authApi = [
'limit' => 10, // max requests per window
'window' => 180, // window in seconds (3 min)
'violation_soft' => 3, // failed attempts before soft block
];
/*
|--------------------------------------------------------------------------
| User-Level Progressive Block Durations (seconds)
| 0 = permanent until manual unblock
|--------------------------------------------------------------------------
*/
public array $userBlock = [
'soft_duration' => 0, // permanent, manual unblock only
'medium_duration' => 7200, // 2 hours
'hard_duration' => 86400, // 24 hours
// attempts while at a block level before escalating to next
'medium_trigger' => 1, // attempts during soft → medium
'hard_trigger' => 1, // attempts during medium → hard
];
/*
|--------------------------------------------------------------------------
| IP-Level Throttle & Progressive Block (independent of user)
|--------------------------------------------------------------------------
*/
public array $ipBlock = [
'limit' => 120, // max requests per window
'window' => 60, // window in seconds
'violation_soft' => 5, // violations before soft block
'soft_duration' => 0, // permanent, manual unblock only
'medium_duration' => 7200, // 2 hours
'hard_duration' => 86400, // 24 hours
'medium_trigger' => 1, // attempts during soft → medium
'hard_trigger' => 1, // attempts during medium → hard
];
/*
|--------------------------------------------------------------------------
| Cache Key Prefixes
|--------------------------------------------------------------------------
*/
public array $cacheKeys = [
'ip_count' => 'rl_ip_count_',
'ip_violations' => 'rl_ip_viol_',
'ip_block' => 'rl_ip_block_',
'ip_block_hits' => 'rl_ip_blkhit_',
'user_count' => 'rl_usr_count_',
'user_violations' => 'rl_usr_viol_',
'user_block' => 'rl_usr_block_',
'user_block_hits' => 'rl_usr_blkhit_',
];
/*
|--------------------------------------------------------------------------
| HTTP Status Codes per block level
|--------------------------------------------------------------------------
*/
public array $statusCodes = [
'throttle' => 429,
'soft' => 429,
'medium' => 403,
'hard' => 451,
];
}

82
app/Config/RfqConfig.php Normal file
View File

@ -0,0 +1,82 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class RfqConfig extends BaseConfig
{
/**
* Google Drive parent folder IDs for RFQ and QCR sheets.
* Values are loaded from .env (RFQ_PARENT_FOLDER_ID).
*/
public string $rfqParentFolderId = '';
public string $qcrParentFolderId = '';
public function __construct()
{
parent::__construct();
$this->rfqParentFolderId = env('RFQ_PARENT_FOLDER_ID', '1MnYh5PTPDlc9mYMGsjmTv02y8EZ-BQf1');
$this->qcrParentFolderId = env('RFQ_PARENT_FOLDER_ID', '1MnYh5PTPDlc9mYMGsjmTv02y8EZ-BQf1');
}
/**
* Default permissions for created sheets.
* Copied from GoogleSheetController::$config.
*/
public array $permissions = [
'editors' => [
'vitvelz@gmail.com',
'velz1990@gmail.com',
'venkateshraman786@gmail.com',
],
'viewers' => [],
];
/**
* Default protections for RFQ sheets.
* Copied from GoogleSheetController::$config.
*/
public array $protections = [
[
'range' => 'RFQ Page!B12:C12',
'users' => [
'velz1990@gmail.com',
'vitvelz@gmail.com',
'firebase-adminsdk-mcdfe@nhance-ee8d1.iam.gserviceaccount.com',
],
'groups' => [],
],
[
'range' => 'Claims Page!A1',
'users' => [
'velz1990@gmail.com',
'venkateshraman786@gmail.com',
'firebase-adminsdk-mcdfe@nhance-ee8d1.iam.gserviceaccount.com',
],
'groups' => [],
],
];
/**
* Placeholder token for past 3 years claims table (written as multi-row values at that cell).
*/
public string $rfqClaimsPlaceholder = '{{CLAIMS_DETAILS}}';
/**
* Maps Google Sheet placeholder => internal key from buildRfqPlaceholderData().
* Add these exact strings in your RFQ templates (any sheet/tab).
*
* @var array<string, string>
*/
public array $rfqPlaceholders = [
'{{INSURED_NAME}}' => 'insured_name',
'{{COMMUNICATION_ADDRESS}}' => 'communication_address',
'{{GST}}' => 'gst',
'{{PAN}}' => 'pan',
'{{POLICY_PERIOD}}' => 'policy_period',
'{{OPPORTUNITY_TYPE}}' => 'opportunity_type',
'{{RISK_LOCATION}}' => 'risk_location',
'{{OCCUPANCY}}' => 'occupancy',
'{{CLAIMS_DETAILS}}' => 'claims_details',
];
}

1325
app/Config/Routes.php Executable file

File diff suppressed because it is too large Load Diff

114
app/Config/Routing.php Executable file
View File

@ -0,0 +1,114 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Config;
use CodeIgniter\Config\Routing as BaseRouting;
/**
* Routing configuration
*/
class Routing extends BaseRouting
{
/**
* An array of files that contain route definitions.
* Route files are read in order, with the first match
* found taking precedence.
*
* Default: APPPATH . 'Config/Routes.php'
*/
public array $routeFiles = [
APPPATH . 'Config/Routes.php',
APPPATH . 'Config/HrLoginDiagnoseRoutes.php',
];
/**
* The default namespace to use for Controllers when no other
* namespace has been specified.
*
* Default: 'App\Controllers'
*/
public string $defaultNamespace = 'App\Controllers';
/**
* The default controller to use when no other controller has been
* specified.
*
* Default: 'Home'
*/
public string $defaultController = 'Home';
/**
* The default method to call on the controller when no other
* method has been set in the route.
*
* Default: 'index'
*/
public string $defaultMethod = 'index';
/**
* Whether to translate dashes in URIs to underscores.
* Primarily useful when using the auto-routing.
*
* Default: false
*/
public bool $translateURIDashes = false;
/**
* Sets the class/method that should be called if routing doesn't
* find a match. It can be either a closure or the controller/method
* name exactly like a route is defined: Users::index
*
* This setting is passed to the Router class and handled there.
*
* If you want to use a closure, you will have to set it in the
* class constructor or the routes file by calling:
*
* $routes->set404Override(function() {
* // Do something here
* });
*
* Example:
* public $override404 = 'App\Errors::show404';
*/
public ?string $override404 = null;
/**
* If TRUE, the system will attempt to match the URI against
* Controllers by matching each segment against folders/files
* in APPPATH/Controllers, when a match wasn't found against
* defined routes.
*
* If FALSE, will stop searching and do NO automatic routing.
*/
public bool $autoRoute = false;
/**
* If TRUE, will enable the use of the 'prioritize' option
* when defining routes.
*
* Default: false
*/
public bool $prioritize = false;
/**
* Map of URI segments and namespaces. For Auto Routing (Improved).
*
* The key is the first URI segment. The value is the controller namespace.
* E.g.,
* [
* 'blog' => 'Acme\Blog\Controllers',
* ]
*
* @var array [ uri_segment => namespace ]
*/
public array $moduleRoutes = [];
}

101
app/Config/Security.php Executable file
View File

@ -0,0 +1,101 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class Security extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* CSRF Protection Method
* --------------------------------------------------------------------------
*
* Protection Method for Cross Site Request Forgery protection.
*
* @var string 'cookie' or 'session'
*/
public string $csrfProtection = 'cookie';
/**
* --------------------------------------------------------------------------
* CSRF Token Randomization
* --------------------------------------------------------------------------
*
* Randomize the CSRF Token for added security.
*/
public bool $tokenRandomize = true;
/**
* --------------------------------------------------------------------------
* CSRF Token Name
* --------------------------------------------------------------------------
*
* Token name for Cross Site Request Forgery protection.
*/
public string $tokenName = 'csrf_test_name';
/**
* --------------------------------------------------------------------------
* CSRF Header Name
* --------------------------------------------------------------------------
*
* Header name for Cross Site Request Forgery protection.
*/
public string $headerName = 'X-CSRF-TOKEN';
/**
* --------------------------------------------------------------------------
* CSRF Cookie Name
* --------------------------------------------------------------------------
*
* Cookie name for Cross Site Request Forgery protection.
*/
public string $cookieName = 'csrf_cookie_name';
/**
* --------------------------------------------------------------------------
* CSRF Expires
* --------------------------------------------------------------------------
*
* Expiration time for Cross Site Request Forgery protection cookie.
*
* Defaults to two hours (in seconds).
*/
public int $expires = 7200;
/**
* --------------------------------------------------------------------------
* CSRF Regenerate
* --------------------------------------------------------------------------
*
* Regenerate CSRF Token on every submission.
*/
public bool $regenerate = false;
/**
* --------------------------------------------------------------------------
* CSRF Redirect
* --------------------------------------------------------------------------
*
* Redirect to previous page with error on failure.
*/
public bool $redirect = true;
/**
* --------------------------------------------------------------------------
* CSRF SameSite
* --------------------------------------------------------------------------
*
* Setting for CSRF SameSite cookie token.
*
* Allowed values are: None - Lax - Strict - ''.
*
* Defaults to `Lax` as recommended in this link:
*
* @see https://portswigger.net/web-security/csrf/samesite-cookies
*
* @deprecated `Config\Cookie` $samesite property is used.
*/
public string $samesite = 'Lax';
}

114
app/Config/Services.php Executable file
View File

@ -0,0 +1,114 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseService;
use App\Libraries\Slug;
use App\Libraries\MyLogger;
use App\Libraries\GmailAPI;
use App\Libraries\MyGoogleDrive;
use App\Libraries\RuleImportService;
use App\Libraries\DataServiceSqlite;
use App\Libraries\S3Service;
use App\Libraries\FileStorageService;
use App\Controllers\Home;
/**
* Services Configuration file.
*
* Services are simply other classes/libraries that the system uses
* to do its job. This is used by CodeIgniter to allow the core of the
* framework to be swapped out easily without affecting the usage within
* the rest of your application.
*
* This file holds any application-specific services, or service overrides
* that you might need. An example has been included with the general
* method format you should use for your service methods. For more examples,
* see the core Services file at system/Config/Services.php.
*/
class Services extends BaseService
{
public static function slug($getShared = true)
{
if ($getShared) {
return static::getSharedInstance('slug');
}
return new Slug();
}
public static function home($getShared = true)
{
if ($getShared) {
return static::getSharedInstance('home');
}
return new Home();
}
public static function mylogger($getShared = true)
{
if ($getShared) {
return static::getSharedInstance('mylogger');
}
return new MyLogger();
}
public static function dataServiceSqlite($getShared = true)
{
if ($getShared) {
return static::getSharedInstance('dataServiceSqlite');
}
return new DataServiceSqlite();
}
public static function gmailapi($getShared = true)
{
if ($getShared) {
return static::getSharedInstance('gmailapi');
}
return new GmailAPI();
}
public static function myGoogleDrive($getShared = true)
{
if ($getShared) {
return static::getSharedInstance('myGoogleDrive');
}
return new MyGoogleDrive();
}
public static function ruleImportService($getShared = true)
{
if ($getShared) {
return static::getSharedInstance('ruleImportService');
}
return new RuleImportService();
}
public static function getS3Service($getShared = true)
{
if ($getShared) {
return static::getSharedInstance('getS3Service');
}
return new S3Service();
}
public static function getFileStorageService($getShared = true)
{
if ($getShared) {
return static::getSharedInstance('getFileStorageService');
}
return new FileStorageService();
}
}

102
app/Config/Session.php Executable file
View File

@ -0,0 +1,102 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Session\Handlers\BaseHandler;
use CodeIgniter\Session\Handlers\FileHandler;
class Session extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Session Driver
* --------------------------------------------------------------------------
*
* The session storage driver to use:
* - `CodeIgniter\Session\Handlers\FileHandler`
* - `CodeIgniter\Session\Handlers\DatabaseHandler`
* - `CodeIgniter\Session\Handlers\MemcachedHandler`
* - `CodeIgniter\Session\Handlers\RedisHandler`
*
* @var class-string<BaseHandler>
*/
public string $driver = FileHandler::class;
/**
* --------------------------------------------------------------------------
* Session Cookie Name
* --------------------------------------------------------------------------
*
* The session cookie name, must contain only [0-9a-z_-] characters
*/
public string $cookieName = 'ci_session';
/**
* --------------------------------------------------------------------------
* Session Expiration
* --------------------------------------------------------------------------
*
* The number of SECONDS you want the session to last.
* Setting to 0 (zero) means expire when the browser is closed.
*/
public int $expiration = 7200;
/**
* --------------------------------------------------------------------------
* Session Save Path
* --------------------------------------------------------------------------
*
* The location to save sessions to and is driver dependent.
*
* For the 'files' driver, it's a path to a writable directory.
* WARNING: Only absolute paths are supported!
*
* For the 'database' driver, it's a table name.
* Please read up the manual for the format with other session drivers.
*
* IMPORTANT: You are REQUIRED to set a valid save path!
*/
public string $savePath = WRITEPATH . 'session';
/**
* --------------------------------------------------------------------------
* Session Match IP
* --------------------------------------------------------------------------
*
* Whether to match the user's IP address when reading the session data.
*
* WARNING: If you're using the database driver, don't forget to update
* your session table's PRIMARY KEY when changing this setting.
*/
public bool $matchIP = false;
/**
* --------------------------------------------------------------------------
* Session Time to Update
* --------------------------------------------------------------------------
*
* How many seconds between CI regenerating the session ID.
*/
public int $timeToUpdate = 300;
/**
* --------------------------------------------------------------------------
* Session Regenerate Destroy
* --------------------------------------------------------------------------
*
* Whether to destroy session data associated with the old session ID
* when auto-regenerating the session ID. When set to FALSE, the data
* will be later deleted by the garbage collector.
*/
public bool $regenerateDestroy = false;
/**
* --------------------------------------------------------------------------
* Session Database Group
* --------------------------------------------------------------------------
*
* DB Group for the database session.
*/
public ?string $DBGroup = null;
}

118
app/Config/Toolbar.php Executable file
View File

@ -0,0 +1,118 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Debug\Toolbar\Collectors\Database;
use CodeIgniter\Debug\Toolbar\Collectors\Events;
use CodeIgniter\Debug\Toolbar\Collectors\Files;
use CodeIgniter\Debug\Toolbar\Collectors\Logs;
use CodeIgniter\Debug\Toolbar\Collectors\Routes;
use CodeIgniter\Debug\Toolbar\Collectors\Timers;
use CodeIgniter\Debug\Toolbar\Collectors\Views;
/**
* --------------------------------------------------------------------------
* Debug Toolbar
* --------------------------------------------------------------------------
*
* The Debug Toolbar provides a way to see information about the performance
* and state of your application during that page display. By default it will
* NOT be displayed under production environments, and will only display if
* `CI_DEBUG` is true, since if it's not, there's not much to display anyway.
*/
class Toolbar extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Toolbar Collectors
* --------------------------------------------------------------------------
*
* List of toolbar collectors that will be called when Debug Toolbar
* fires up and collects data from.
*
* @var string[]
*/
public array $collectors = [
Timers::class,
Database::class,
Logs::class,
Views::class,
// \CodeIgniter\Debug\Toolbar\Collectors\Cache::class,
Files::class,
Routes::class,
Events::class,
];
/**
* --------------------------------------------------------------------------
* Collect Var Data
* --------------------------------------------------------------------------
*
* If set to false var data from the views will not be colleted. Useful to
* avoid high memory usage when there are lots of data passed to the view.
*/
public bool $collectVarData = true;
/**
* --------------------------------------------------------------------------
* Max History
* --------------------------------------------------------------------------
*
* `$maxHistory` sets a limit on the number of past requests that are stored,
* helping to conserve file space used to store them. You can set it to
* 0 (zero) to not have any history stored, or -1 for unlimited history.
*/
public int $maxHistory = 20;
/**
* --------------------------------------------------------------------------
* Toolbar Views Path
* --------------------------------------------------------------------------
*
* The full path to the the views that are used by the toolbar.
* This MUST have a trailing slash.
*/
public string $viewsPath = SYSTEMPATH . 'Debug/Toolbar/Views/';
/**
* --------------------------------------------------------------------------
* Max Queries
* --------------------------------------------------------------------------
*
* If the Database Collector is enabled, it will log every query that the
* the system generates so they can be displayed on the toolbar's timeline
* and in the query log. This can lead to memory issues in some instances
* with hundreds of queries.
*
* `$maxQueries` defines the maximum amount of queries that will be stored.
*/
public int $maxQueries = 100;
/**
* --------------------------------------------------------------------------
* Watched Directories
* --------------------------------------------------------------------------
*
* Contains an array of directories that will be watched for changes and
* used to determine if the hot-reload feature should reload the page or not.
* We restrict the values to keep performance as high as possible.
*
* NOTE: The ROOTPATH will be prepended to all values.
*/
public array $watchedDirectories = [
'app',
];
/**
* --------------------------------------------------------------------------
* Watched File Extensions
* --------------------------------------------------------------------------
*
* Contains an array of file extensions that will be watched for changes and
* used to determine if the hot-reload feature should reload the page or not.
*/
public array $watchedExtensions = [
'php', 'css', 'js', 'html', 'svg', 'json', 'env',
];
}

252
app/Config/UserAgents.php Executable file
View File

@ -0,0 +1,252 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
/**
* -------------------------------------------------------------------
* User Agents
* -------------------------------------------------------------------
*
* This file contains four arrays of user agent data. It is used by the
* User Agent Class to help identify browser, platform, robot, and
* mobile device data. The array keys are used to identify the device
* and the array values are used to set the actual name of the item.
*/
class UserAgents extends BaseConfig
{
/**
* -------------------------------------------------------------------
* OS Platforms
* -------------------------------------------------------------------
*
* @var array<string, string>
*/
public array $platforms = [
'windows nt 10.0' => 'Windows 10',
'windows nt 6.3' => 'Windows 8.1',
'windows nt 6.2' => 'Windows 8',
'windows nt 6.1' => 'Windows 7',
'windows nt 6.0' => 'Windows Vista',
'windows nt 5.2' => 'Windows 2003',
'windows nt 5.1' => 'Windows XP',
'windows nt 5.0' => 'Windows 2000',
'windows nt 4.0' => 'Windows NT 4.0',
'winnt4.0' => 'Windows NT 4.0',
'winnt 4.0' => 'Windows NT',
'winnt' => 'Windows NT',
'windows 98' => 'Windows 98',
'win98' => 'Windows 98',
'windows 95' => 'Windows 95',
'win95' => 'Windows 95',
'windows phone' => 'Windows Phone',
'windows' => 'Unknown Windows OS',
'android' => 'Android',
'blackberry' => 'BlackBerry',
'iphone' => 'iOS',
'ipad' => 'iOS',
'ipod' => 'iOS',
'os x' => 'Mac OS X',
'ppc mac' => 'Power PC Mac',
'freebsd' => 'FreeBSD',
'ppc' => 'Macintosh',
'linux' => 'Linux',
'debian' => 'Debian',
'sunos' => 'Sun Solaris',
'beos' => 'BeOS',
'apachebench' => 'ApacheBench',
'aix' => 'AIX',
'irix' => 'Irix',
'osf' => 'DEC OSF',
'hp-ux' => 'HP-UX',
'netbsd' => 'NetBSD',
'bsdi' => 'BSDi',
'openbsd' => 'OpenBSD',
'gnu' => 'GNU/Linux',
'unix' => 'Unknown Unix OS',
'symbian' => 'Symbian OS',
];
/**
* -------------------------------------------------------------------
* Browsers
* -------------------------------------------------------------------
*
* The order of this array should NOT be changed. Many browsers return
* multiple browser types so we want to identify the subtype first.
*
* @var array<string, string>
*/
public array $browsers = [
'OPR' => 'Opera',
'Flock' => 'Flock',
'Edge' => 'Spartan',
'Edg' => 'Edge',
'Chrome' => 'Chrome',
// Opera 10+ always reports Opera/9.80 and appends Version/<real version> to the user agent string
'Opera.*?Version' => 'Opera',
'Opera' => 'Opera',
'MSIE' => 'Internet Explorer',
'Internet Explorer' => 'Internet Explorer',
'Trident.* rv' => 'Internet Explorer',
'Shiira' => 'Shiira',
'Firefox' => 'Firefox',
'Chimera' => 'Chimera',
'Phoenix' => 'Phoenix',
'Firebird' => 'Firebird',
'Camino' => 'Camino',
'Netscape' => 'Netscape',
'OmniWeb' => 'OmniWeb',
'Safari' => 'Safari',
'Mozilla' => 'Mozilla',
'Konqueror' => 'Konqueror',
'icab' => 'iCab',
'Lynx' => 'Lynx',
'Links' => 'Links',
'hotjava' => 'HotJava',
'amaya' => 'Amaya',
'IBrowse' => 'IBrowse',
'Maxthon' => 'Maxthon',
'Ubuntu' => 'Ubuntu Web Browser',
'Vivaldi' => 'Vivaldi',
];
/**
* -------------------------------------------------------------------
* Mobiles
* -------------------------------------------------------------------
*
* @var array<string, string>
*/
public array $mobiles = [
// legacy array, old values commented out
'mobileexplorer' => 'Mobile Explorer',
// 'openwave' => 'Open Wave',
// 'opera mini' => 'Opera Mini',
// 'operamini' => 'Opera Mini',
// 'elaine' => 'Palm',
'palmsource' => 'Palm',
// 'digital paths' => 'Palm',
// 'avantgo' => 'Avantgo',
// 'xiino' => 'Xiino',
'palmscape' => 'Palmscape',
// 'nokia' => 'Nokia',
// 'ericsson' => 'Ericsson',
// 'blackberry' => 'BlackBerry',
// 'motorola' => 'Motorola'
// Phones and Manufacturers
'motorola' => 'Motorola',
'nokia' => 'Nokia',
'palm' => 'Palm',
'iphone' => 'Apple iPhone',
'ipad' => 'iPad',
'ipod' => 'Apple iPod Touch',
'sony' => 'Sony Ericsson',
'ericsson' => 'Sony Ericsson',
'blackberry' => 'BlackBerry',
'cocoon' => 'O2 Cocoon',
'blazer' => 'Treo',
'lg' => 'LG',
'amoi' => 'Amoi',
'xda' => 'XDA',
'mda' => 'MDA',
'vario' => 'Vario',
'htc' => 'HTC',
'samsung' => 'Samsung',
'sharp' => 'Sharp',
'sie-' => 'Siemens',
'alcatel' => 'Alcatel',
'benq' => 'BenQ',
'ipaq' => 'HP iPaq',
'mot-' => 'Motorola',
'playstation portable' => 'PlayStation Portable',
'playstation 3' => 'PlayStation 3',
'playstation vita' => 'PlayStation Vita',
'hiptop' => 'Danger Hiptop',
'nec-' => 'NEC',
'panasonic' => 'Panasonic',
'philips' => 'Philips',
'sagem' => 'Sagem',
'sanyo' => 'Sanyo',
'spv' => 'SPV',
'zte' => 'ZTE',
'sendo' => 'Sendo',
'nintendo dsi' => 'Nintendo DSi',
'nintendo ds' => 'Nintendo DS',
'nintendo 3ds' => 'Nintendo 3DS',
'wii' => 'Nintendo Wii',
'open web' => 'Open Web',
'openweb' => 'OpenWeb',
// Operating Systems
'android' => 'Android',
'symbian' => 'Symbian',
'SymbianOS' => 'SymbianOS',
'elaine' => 'Palm',
'series60' => 'Symbian S60',
'windows ce' => 'Windows CE',
// Browsers
'obigo' => 'Obigo',
'netfront' => 'Netfront Browser',
'openwave' => 'Openwave Browser',
'mobilexplorer' => 'Mobile Explorer',
'operamini' => 'Opera Mini',
'opera mini' => 'Opera Mini',
'opera mobi' => 'Opera Mobile',
'fennec' => 'Firefox Mobile',
// Other
'digital paths' => 'Digital Paths',
'avantgo' => 'AvantGo',
'xiino' => 'Xiino',
'novarra' => 'Novarra Transcoder',
'vodafone' => 'Vodafone',
'docomo' => 'NTT DoCoMo',
'o2' => 'O2',
// Fallback
'mobile' => 'Generic Mobile',
'wireless' => 'Generic Mobile',
'j2me' => 'Generic Mobile',
'midp' => 'Generic Mobile',
'cldc' => 'Generic Mobile',
'up.link' => 'Generic Mobile',
'up.browser' => 'Generic Mobile',
'smartphone' => 'Generic Mobile',
'cellphone' => 'Generic Mobile',
];
/**
* -------------------------------------------------------------------
* Robots
* -------------------------------------------------------------------
*
* There are hundred of bots but these are the most common.
*
* @var array<string, string>
*/
public array $robots = [
'googlebot' => 'Googlebot',
'msnbot' => 'MSNBot',
'baiduspider' => 'Baiduspider',
'bingbot' => 'Bing',
'slurp' => 'Inktomi Slurp',
'yahoo' => 'Yahoo',
'ask jeeves' => 'Ask Jeeves',
'fastcrawler' => 'FastCrawler',
'infoseek' => 'InfoSeek Robot 1.0',
'lycos' => 'Lycos',
'yandex' => 'YandexBot',
'mediapartners-google' => 'MediaPartners Google',
'CRAZYWEBCRAWLER' => 'Crazy Webcrawler',
'adsbot-google' => 'AdsBot Google',
'feedfetcher-google' => 'Feedfetcher Google',
'curious george' => 'Curious George',
'ia_archiver' => 'Alexa Crawler',
'MJ12bot' => 'Majestic-12',
'Uptimebot' => 'Uptimebot',
];
}

44
app/Config/Validation.php Executable file
View File

@ -0,0 +1,44 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Validation\StrictRules\CreditCardRules;
use CodeIgniter\Validation\StrictRules\FileRules;
use CodeIgniter\Validation\StrictRules\FormatRules;
use CodeIgniter\Validation\StrictRules\Rules;
class Validation extends BaseConfig
{
// --------------------------------------------------------------------
// Setup
// --------------------------------------------------------------------
/**
* Stores the classes that contain the
* rules that are available.
*
* @var string[]
*/
public array $ruleSets = [
Rules::class,
FormatRules::class,
FileRules::class,
CreditCardRules::class,
];
/**
* Specifies the views that are used to display the
* errors.
*
* @var array<string, string>
*/
public array $templates = [
'list' => 'CodeIgniter\Validation\Views\list',
'single' => 'CodeIgniter\Validation\Views\single',
];
// --------------------------------------------------------------------
// Rules
// --------------------------------------------------------------------
}

62
app/Config/View.php Executable file
View File

@ -0,0 +1,62 @@
<?php
namespace Config;
use CodeIgniter\Config\View as BaseView;
use CodeIgniter\View\ViewDecoratorInterface;
/**
* @phpstan-type ParserCallable (callable(mixed): mixed)
* @phpstan-type ParserCallableString (callable(mixed): mixed)&string
*/
class View extends BaseView
{
/**
* When false, the view method will clear the data between each
* call. This keeps your data safe and ensures there is no accidental
* leaking between calls, so you would need to explicitly pass the data
* to each view. You might prefer to have the data stick around between
* calls so that it is available to all views. If that is the case,
* set $saveData to true.
*
* @var bool
*/
public $saveData = true;
/**
* Parser Filters map a filter name with any PHP callable. When the
* Parser prepares a variable for display, it will chain it
* through the filters in the order defined, inserting any parameters.
* To prevent potential abuse, all filters MUST be defined here
* in order for them to be available for use within the Parser.
*
* Examples:
* { title|esc(js) }
* { created_on|date(Y-m-d)|esc(attr) }
*
* @var array<string, string>
* @phpstan-var array<string, ParserCallableString>
*/
public $filters = [];
/**
* Parser Plugins provide a way to extend the functionality provided
* by the core Parser by creating aliases that will be replaced with
* any callable. Can be single or tag pair.
*
* @var array<string, array<string>|callable|string>
* @phpstan-var array<string, array<ParserCallableString>|ParserCallableString|ParserCallable>
*/
public $plugins = [];
/**
* View Decorators are class methods that will be run in sequence to
* have a chance to alter the generated output just prior to caching
* the results.
*
* All classes must implement CodeIgniter\View\ViewDecoratorInterface
*
* @var class-string<ViewDecoratorInterface>[]
*/
public array $decorators = [];
}

13
app/Config/google-services.json Executable file
View File

@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "push-notification-enrollment",
"private_key_id": "979d0fcd0ce95d31367f3150d34ee0044ca51b0d",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4Xsan6ESIzM12\nG15gRU361Yy6PVmoMzTToWtZHPEcDRd9weFNlCXjmX+WAqdgD/hQ42GcZQVCndse\nEQGJHbRu/ifMKGo53zxOpshNZnCSxojAGWIGrIHFE5kFumiKz9drW9vPWsFLtgx0\nEAW0T7su1MejjH33GftHG8k/q3uyWoU0+OFRD6emhYkFdVsucwR27hE7GsFwE+CF\nnRdSWQ5mu2e8J7q9Qbpgot5d7uk7iJgpiyNdChPvqUZ7XeknVrw1F4REZlbAkVjb\nFuwKg5qtG97LZ1NkyYqFmssA1rNkqSi4+T+2/ERegdwyA6W4/HLT9fPbSl4v1MvB\ntJlGYkMpAgMBAAECggEABhspdCUmo+s4gMleQkz5TK3m58IhZocvoDSv4/cn6xhp\n50HVDucxrAyI47R5y54ryK4HLRFRb8ffmmrQxLRFpgln0wShpAIHMsmmR531a38E\nx2vvya3L7HV+M2jdjn9csJMNwBvO3A2O8wcW0UZ0uhPU+s8r2tOy9UNv0lqBEcKM\nUxCtk7UxcLEM3btDP5I4oEHVo2f5jzPeb99KHbYQV/vc0BtA1wtuYTzGT2EhBpTG\nN1rbQhlj6iJ6vMwHETWrctNMohCQNQEv/QIC/NWYPNRqmtrJfNe29XGWN9IIhv0e\n/ZmhdzoO+uhJW4VaWpI93nB5ZAa6LHqJSg+OyrPoawKBgQDd45Aiq7d+4Z+kDZYu\n72bgW5CGhyck2hrqHpBdHA960AYIftw/wHgI9xb9Z8hi+Lh+W18xDywOLoaz7TJV\nElj2Gh3oj2ZLzDpeyqV9UOgz11dbm6yYUK8jl7zyajBt7RhGcdj+4x+YjUTT6O61\nuxm2ZbKsb/0er/W6DlKDVvpWSwKBgQDUtqowATEaGTrd+eutanRwtv+rdptVcadj\nD4H3XdY4PAsVSp1mT9knGgh7c5elI0lBH/VD/IbM5uPXOc7judnIK4RgKScrJT2P\nekyEJnXyFAZGS3WTFG7fzeXn/cNNx5MCZE2Yay7LqqON8Kg08IvvesiTushrDlMJ\nX8cunSyz2wKBgDIOwZigxq/QNNSs4AHMrpfU8GD5IqKUtde1d3oZ94AMaCAIhqW3\nRR04qS4X+MQjOnP/JxWJR7YXVvpGe8FndzxmHfM2Tqyw8UYrT3RbCVeQsDuRfjmK\nkkhkVhMWU8Co6X4S9xJhqOIglLN97ESBZkaY4Ns4FJGUvsnvqzvIJofLAoGBAKJO\ny2eb0TK/46ozJEELxNOo30efVgGJmpa844e0A1yffDl/2MCT1ve+JpDEcAbi+OeH\nkieRTe6Vk27LvnEHhAT4J6cUX73NSb7sK+x+SGsyGmOS+qEC62M8gdxWRqtXyHX/\nwTG3P1rK1sfcxQy4K57NSrVmxbzijjvN6HdKGS0XAoGATtsid5pypcZ0BsYUp/27\neS6z++ewo7QCVCBPa4Ag4nX56/0kdrECvy/ISMDjggNv+mR8hpyCjfm9ztJ88njB\nUBu1jzLBZ+u5gEjmtu43qvuDJNobyb+evDSReEJmify943vFZqnkIhFlwg1PcS6S\nCd+pr559KWURPIgfxb7S/JU=\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-gzzkw@push-notification-enrollment.iam.gserviceaccount.com",
"client_id": "109128197176743587316",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-gzzkw%40push-notification-enrollment.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\ControllerCleaners;
use App\Controllers\BaseController;
use CodeIgniter\HTTP\ResponseInterface;
use App\Models\ThzMasterModel;
use App\Models\ThzMasterNotesModel;
class ThzControllerCleaner extends BaseController
{
protected $thzMasterModel;
protected $thzMasterNotesModel;
public function __construct()
{
$this->thzMasterModel = new ThzMasterModel();
$this->thzMasterNotesModel = new ThzMasterNotesModel();
}
public function index()
{
//
}
public function ticketCreation($data){
if(empty($data)){
return false;
}
$cleanedData = $data;
return $cleanedData;
}
public function ticketList($data){
if(empty($data)){
return false;
}
$cleanedData = $data;
return $cleanedData;
}
public function ticketConversation($data){
if(empty($data)){
return false;
}
$cleanedData = $data;
return $cleanedData;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Controllers;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
use App\Controllers\PublicController;
//This is a controller act like base controller for business logics which are required
// authendication and authrizations also config the routes as per requirement in routes.php
class AdminController extends BaseController
{
public function __construct()
{
}
}

View File

@ -0,0 +1,829 @@
<?php
namespace App\Controllers\Api;
use App\Controllers\BaseController;
use CodeIgniter\API\ResponseTrait;
use App\Models\NonEbTicketMasterModel;
use App\Models\NonEbClaimAssetModel;
use App\Models\TicketHistoryModel;
use App\Models\TicketClaimStatusModel;
use App\Models\ClaimFilesModel;
use App\Models\ClientPolicyModel;
use App\Models\PolicyTypeModel;
use App\Models\ClientRMModel;
use App\Models\EmployeeModel;
use App\Models\LevelContactModel;
use App\Helpers\JWTToken;
class NonEbClaimApiController extends BaseController
{
use ResponseTrait;
protected $nonEbTicketModel;
protected $assetModel;
protected $ticketHistoryModel;
protected $claimStatusModel;
protected $claimFilesModel;
protected $clientPolicyModel;
protected $policyTypeModel;
protected $clientRMModel;
protected $myLogger;
// API system user ID — used as created_by for all inserts (no session)
const API_SYSTEM_USER_ID = 0;
public function __construct()
{
$this->nonEbTicketModel = new NonEbTicketMasterModel();
$this->assetModel = new NonEbClaimAssetModel();
$this->ticketHistoryModel = new TicketHistoryModel();
$this->claimStatusModel = new TicketClaimStatusModel();
$this->claimFilesModel = new ClaimFilesModel();
$this->clientPolicyModel = new ClientPolicyModel();
$this->policyTypeModel = new PolicyTypeModel();
$this->clientRMModel = new ClientRMModel();
$this->myLogger = \Config\Services::mylogger();
}
// ===================== AUTH HELPER =====================
/**
* Decode the JWT from Authorization header and return the resolved user row.
* Returns array with normalised keys: id, name, email, mobile.
* Returns null if token is missing or invalid.
*/
protected function getAuthUser(): ?array
{
$authHeader = $this->request->getHeaderLine('Authorization');
if (empty($authHeader)) {
return null;
}
$result = JWTToken::validateJWT($authHeader);
if ($result['status'] !== true) {
return null;
}
$decoded = $result['decoded'];
if (isset($decoded['emp_code'])) {
$model = new EmployeeModel();
$user = $model->find($decoded['id'] ?? null);
if (!$user) return null;
return [
'id' => $user['id'],
'name' => $user['name'] ?? '',
'email' => $user['email_corporate'] ?? $user['email'] ?? '',
'mobile' => $user['mobile'] ?? '',
];
}
$model = new LevelContactModel();
$user = $model->find($decoded['post_hr_id'] ?? null);
if (!$user) return null;
return [
'id' => $user['id'],
'name' => $user['name'] ?? '',
'email' => $user['email'] ?? '',
'mobile' => $user['mobile'] ?? '',
];
}
// ===================== SHARED HELPERS =====================
/**
* Convert DD-MM-YYYY date fields to Y-m-d for DB storage.
*/
protected function formatDatesForClaim(array $data): array
{
$dateFields = ['loss_date', 'intimation_recd_date', 'intimated_to_insurer_date', 'eta_for_documents'];
foreach ($dateFields as $field) {
if (empty($data[$field])) {
$data[$field] = null;
} else {
$data[$field] = change_date_format($data[$field], null, 'Y-m-d');
}
}
return $data;
}
/**
* Duplicate check: same client + loss_date + policy_no.
*/
protected function checkDuplicateNonEbClaim(array $data): bool
{
$client_id = $data['client_id'] ?? null;
$loss_date = $data['loss_date'] ?? null;
$policy_no = $data['policy_no'] ?? null;
if (empty($client_id) || empty($loss_date)) return false;
$query = $this->nonEbTicketModel
->where('client_id', $client_id)
->where('loss_date', $loss_date)
->where('is_active', 1);
if (!empty($policy_no)) {
$query->where('policy_no', $policy_no);
}
return !empty($query->first());
}
/**
* Upload the asset_file from the request. Returns filename or null.
*/
protected function handleAssetFileUpload(): ?string
{
$file = $this->request->getFile('asset_file');
if ($file === null || !$file->isValid() || $file->hasMoved()) {
return null;
}
$uploadPath = WRITEPATH . 'uploads/non_eb_asset_files/';
$fileName = storage_file_Upload($file, $uploadPath, UPLOAD_EXT_ASSET_FILES);
return !empty($fileName) ? $fileName : null;
}
/**
* Save asset rows (asset_id_code[], serial_no[], etc.) linked to a claim.
*/
protected function saveAssets(int $claim_id, array $post_data): void
{
$this->assetModel->where('non_eb_ticket_id', $claim_id)->set(['is_active' => 0])->update();
$codes = $post_data['asset_id_code'] ?? [];
$serials = $post_data['serial_no'] ?? [];
$vehicles = $post_data['vehicle_no'] ?? [];
$descriptions = $post_data['asset_description'] ?? [];
if (!is_array($codes)) return;
for ($i = 0; $i < count($codes); $i++) {
$code = trim($codes[$i] ?? '');
$serial = trim($serials[$i] ?? '');
$vehicle = trim($vehicles[$i] ?? '');
$desc = trim($descriptions[$i] ?? '');
if (empty($code) && empty($serial) && empty($vehicle) && empty($desc)) continue;
$this->assetModel->insert([
'non_eb_ticket_id' => $claim_id,
'asset_id_code' => $code,
'serial_no' => $serial,
'vehicle_no' => $vehicle,
'asset_description' => $desc,
'is_active' => 1,
]);
}
}
/**
* Insert the initial history row after a new claim is created.
*/
protected function putHistoryAfterInsert(array $ticket_data, int $ticket_id): void
{
if (!empty($ticket_data)) {
$this->ticketHistoryModel->insert([
'ticket_id' => $ticket_id,
'field_name' => 'claim_status_id',
'display_name' => 'Claim Created',
'old_value' => null,
'new_value' => $ticket_data['claim_status_id'],
'created_by' => self::API_SYSTEM_USER_ID,
'is_active' => 1,
]);
}
}
// ===================== ENDPOINTS =====================
public function createClaim()
{
$authUser = $this->getAuthUser();
if (!$authUser) {
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
}
$rawBody = $this->request->getBody();
$isJson = str_contains($this->request->getHeaderLine('Content-Type'), 'application/json');
$body = ($isJson || ($rawBody !== '' && $rawBody !== null))
? (json_decode($rawBody, true) ?? $this->request->getPost())
: $this->request->getPost();
// Validate minimal user-facing fields only
$rules = [
'client_policy_id' => ['rules' => 'required|is_natural_no_zero', 'errors' => ['required' => 'Client Policy is required']],
'nature_of_loss' => ['rules' => 'required|min_length[3]', 'errors' => ['required' => 'Nature of Loss is required']],
'loss_location' => ['rules' => 'required', 'errors' => ['required' => 'Loss Location is required']],
'loss_date' => ['rules' => 'required', 'errors' => ['required' => 'Loss Date is required']],
'loss_description' => ['rules' => 'permit_empty'],
'loss_estimate' => ['rules' => 'permit_empty|numeric'],
'claim_number' => ['rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9\/_-]+$/]'],
];
if (!$this->validateData($body, $rules)) {
return $this->respond([
'status' => false, 'code' => 400,
'message' => 'Input validation failed',
'errors' => $this->validator->getErrors(),
], 400);
}
// Fetch client_policy — derive all FK fields from it
$cp = $this->clientPolicyModel
->where('id', (int)$body['client_policy_id'])
->where('is_active', 1)
->first();
if (!$cp) {
return $this->respond(['status' => false, 'code' => 404, 'message' => 'Client policy not found or inactive'], 404);
}
// Validate policy type is Non-EB or Marine
$policyType = $this->policyTypeModel
->select('allocg')
->where('id', $cp['policy_type_id'])
->where('is_active', 1)
->first();
if (!$policyType || !in_array($policyType['allocg'], ['Non-EB', 'Marine'])) {
return $this->respond(['status' => false, 'code' => 422, 'message' => 'Only Non-EB or Marine policy types are allowed'], 422);
}
// Fetch ACM from client_rm (level 3)
$acm = $this->clientRMModel
->where('client_id', $cp['client_id'])
->where('level', 3)
->where('is_active', 1)
->first();
if (!$acm) {
$this->myLogger->logme('error', "[NON_EB_API][createClaim] No ACM found for client_id: {$cp['client_id']}");
}
// Auto-set first claim_status_id for this policy type
$firstStatus = $this->claimStatusModel
->select('id')
->where('ticket_type', $cp['policy_type_id'])
->orderBy('id', 'ASC')
->first();
if (!$firstStatus) {
return $this->respond(['status' => false, 'code' => 422, 'message' => 'No claim status configured for this policy type'], 422);
}
// Build ticket data — merge user input with server-derived values
$ticket_data = [
'client_policy_id' => (int)$cp['id'],
'client_id' => (int)$cp['client_id'],
'branch_id' => (int)$cp['client_branch_id'],
'policy_type_id' => (int)$cp['policy_type_id'],
'insurer_id' => (int)$cp['insurer_id'],
'policy_no' => $cp['policy_no'] ?? null,
'acm_id' => $acm ? (int)$acm['user_id'] : null,
'claim_status_id' => (int)$firstStatus['id'],
'insured_contact_name' => $authUser['name'],
'insured_contact_number' => $authUser['mobile'],
'insured_contact_email' => $authUser['email'],
'nature_of_loss' => $body['nature_of_loss'],
'loss_location' => $body['loss_location'],
'loss_date' => $body['loss_date'],
'loss_description' => $body['loss_description'] ?? null,
'loss_estimate' => $body['loss_estimate'] ?? null,
'claim_number' => $body['claim_number'] ?? null,
'priority' => 1,
'created_by' => self::API_SYSTEM_USER_ID,
];
$ticket_data = $this->formatDatesForClaim($ticket_data);
// Handle optional asset file upload
$assetFileName = null;
$assetFile = $this->request->getFile('asset_file');
if ($assetFile !== null && $assetFile->isValid() && !$assetFile->hasMoved()) {
$ext = strtolower($assetFile->getClientExtension());
$allowed = UPLOAD_EXT_ASSET_FILES; // ['pdf', 'xls', 'xlsx', 'csv']
if (!in_array($ext, $allowed)) {
return $this->respond([
'status' => false, 'code' => 415,
'message' => 'Unsupported file type: ' . $ext . '. Allowed: ' . implode(', ', $allowed),
], 415);
}
if (empty(trim($ticket_data['loss_description'] ?? ''))) {
return $this->respond([
'status' => false, 'code' => 400,
'message' => 'Input validation failed',
'errors' => ['loss_description' => 'Loss Description is required when uploading an asset file.'],
], 400);
}
$uploadPath = WRITEPATH . 'uploads/non_eb_asset_files/';
$assetFileName = storage_file_Upload($assetFile, $uploadPath, $allowed);
if (empty($assetFileName)) {
return $this->respond(['status' => false, 'code' => 500, 'message' => 'Asset file upload failed'], 500);
}
$ticket_data['asset_file'] = $assetFileName;
}
// Duplicate check (runs after derivation so client_id + policy_no are populated)
if ($this->checkDuplicateNonEbClaim($ticket_data)) {
return $this->respond([
'status' => false, 'code' => 409,
'message' => 'Duplicate claim found for Client + Loss Date + Policy No combination',
], 409);
}
$claim_id = $this->nonEbTicketModel->insert($ticket_data);
if (!$claim_id) {
return $this->respond(['status' => false, 'code' => 500, 'message' => 'Failed to create claim'], 500);
}
$this->saveAssets($claim_id, $body);
$this->putHistoryAfterInsert($ticket_data, $claim_id);
$this->myLogger->logme('error', "[NON_EB_API][createClaim] Claim created. ID: $claim_id, user: {$authUser['id']}");
return $this->respond([
'status' => true,
'code' => 200,
'claim_id' => $claim_id,
'asset_file' => $assetFileName,
'message' => 'Non-EB Claim created successfully',
], 200);
}
public function listClaims()
{
$authUser = $this->getAuthUser();
if (!$authUser) {
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
}
try {
$body = $this->request->getJSON(true) ?? [];
} catch (\Throwable $e) {
return $this->respond(['status' => false, 'code' => 400, 'message' => 'Invalid JSON body'], 400);
}
$page = max(1, (int)($body['page'] ?? 1));
$per_page = min(100, max(1, (int)($body['per_page'] ?? 20)));
$offset = ($page - 1) * $per_page;
$db = db_connect();
$builder = $db->table('non_eb_ticket_master tm');
$builder->select([
'tm.id',
'tm.claim_number',
'tm.nhance_claim_ref_no',
'tm.policy_no',
'tm.policy_type_id',
'tm.claim_status_id',
'tcs.claim_status AS status',
'tcs.display_name AS status_display',
'pt.policy_type AS policy_type_name',
'c.client_name',
'i.name AS insurer_name',
'tm.loss_date',
'tm.loss_location',
'tm.nature_of_loss',
'tm.loss_estimate',
'tm.insured_contact_name',
'tm.insured_contact_number',
'(SELECT first_name FROM user_profiles WHERE user_profiles.id = tm.acm_id) AS acm_name',
'DATE_FORMAT(tm.created_at, "%d-%m-%Y") AS created_date',
'DATE_FORMAT(tm.updated_at, "%d-%m-%Y") AS updated_date',
]);
$builder->join('clients c', 'c.id = tm.client_id AND c.is_active = 1', 'left');
$builder->join('insurers i', 'i.id = tm.insurer_id AND i.is_active = 1', 'left');
$builder->join('ticket_claim_status tcs','tcs.id = tm.claim_status_id AND tcs.is_active = 1', 'left');
$builder->join('policy_type pt', 'pt.id = tm.policy_type_id AND pt.is_active = 1', 'left');
$builder->where('tm.is_active', 1);
// Filters
if (!empty($body['client_id'])) $builder->where('md5(tm.client_id)', (int)$body['client_id']);
if (!empty($body['insurer_id'])) $builder->where('tm.insurer_id', (int)$body['insurer_id']);
if (!empty($body['policy_type_id'])) $builder->where('tm.policy_type_id', (int)$body['policy_type_id']);
if (!empty($body['claim_status_id'])) $builder->where('tm.claim_status_id', (int)$body['claim_status_id']);
if (!empty($body['claim_number'])) $builder->like('tm.claim_number', $body['claim_number']);
if (!empty($body['nhance_claim_ref_no'])) $builder->like('tm.nhance_claim_ref_no', $body['nhance_claim_ref_no']);
// Date range filter
if (!empty($body['date_type']) && !empty($body['start_date']) && !empty($body['end_date'])) {
$col = $body['date_type'] === 'updated_date' ? 'tm.updated_at' : 'tm.created_at';
$start = date('Y-m-d 00:00:00', strtotime(str_replace('-', '/', $body['start_date'])));
$end = date('Y-m-d 23:59:59', strtotime(str_replace('-', '/', $body['end_date'])));
$builder->where("$col BETWEEN '$start' AND '$end'");
}
// Exclude terminal statuses by default
if (empty($body['show_closed'])) {
$builder->whereNotIn('tcs.display_name', ['Claim Settled', 'Claim Closed', 'Claim Rejected', 'Claim Withdrawn']);
}
// COUNT for pagination (clone before limit)
$countBuilder = clone $builder;
$total = $countBuilder->countAllResults(false);
$builder->orderBy('tm.id', 'DESC');
$builder->limit($per_page, $offset);
$data = $builder->get()->getResultArray();
$this->myLogger->logme('error', "[NON_EB_API][listClaims] user: {$authUser['id']}, page: $page, total: $total");
return $this->respond([
'status' => true,
'code' => 200,
'total' => (int)$total,
'page' => $page,
'per_page' => $per_page,
'data' => $data,
], 200);
}
public function claimHistory(int $claim_id)
{
$authUser = $this->getAuthUser();
if (!$authUser) {
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
}
// Fetch full claim row with joins (client, insurer, status, policy type, ACM, branch)
$raw = $this->nonEbTicketModel->getTicketDataByTicketID($claim_id);
if (!$raw) {
return $this->respond(['status' => false, 'code' => 404, 'message' => 'Claim not found'], 404);
}
// claims_data — major display fields only
$claims_data = [
'id' => $raw['id'],
'nhance_claim_ref_no' => $raw['nhance_claim_ref_no'],
'claim_number' => $raw['claim_number'],
'policy_no' => $raw['policy_no'],
'policy_type_name' => $raw['policy_type_name'],
'client_name' => $raw['client_name'],
'branch_name' => $raw['branch_name'],
'insurer_name' => $raw['insurer_name'],
'claim_status' => $raw['claim_status'],
'status_display_name' => $raw['status_display_name'],
'insured_contact_name' => $raw['insured_contact_name'],
'insured_contact_number' => $raw['insured_contact_number'],
'insured_contact_email' => $raw['insured_contact_email'],
'nature_of_loss' => $raw['nature_of_loss'],
'loss_location' => $raw['loss_location'],
'loss_date' => $raw['loss_date'],
'loss_description' => $raw['loss_description'],
'loss_estimate' => $raw['loss_estimate'],
'loss_assessed_value' => $raw['loss_assessed_value'],
'settled_amount' => $raw['settled_amount'],
'settlement_utr' => $raw['settlement_utr'],
'intimation_recd_date' => $raw['intimation_recd_date'],
'intimated_to_insurer_date'=> $raw['intimated_to_insurer_date'],
'surveyor_name' => $raw['surveyor_name'],
'surveyor_contact_person' => $raw['surveyor_contact_person'],
'surveyor_contact_number' => $raw['surveyor_contact_number'],
'acm' => $raw['acm'],
'acm_mobile' => $raw['acm_mobile'],
'asset_file' => $raw['asset_file'],
'priority' => $raw['priority'],
'created_at' => $raw['created_at'],
'updated_at' => $raw['updated_at'],
];
// required_docs — decoded separately
$required_docs = json_decode($raw['required_docs'] ?? '{}', true) ?: [];
// claims_files
$claims_files = $this->claimFilesModel
->select('id, doc_name, file_name, mime_type, file_type, docs_for_ir, created_at')
->where('ticket_id', $claim_id)
->where('ticket_type', 2)
->where('is_active', 1)
->findAll();
// ticket_data — claim status history, oldest first
$history_rows = $this->ticketHistoryModel
->select('new_value, created_at')
->where('ticket_id', $claim_id)
->where('field_name', 'claim_status_id')
->where('is_active', 1)
->orderBy('created_at', 'ASC')
->findAll();
$statuses = $this->claimStatusModel
->select('id, claim_status, display_name')
->where('ticket_type', $raw['policy_type_id'])
->where('display_name IS NOT NULL')
->where("display_name != ''")
->where('is_active', 1)
->findAll();
$display_map = array_column($statuses, 'display_name', 'id');
$ticket_data = [];
foreach ($history_rows as $row) {
$status_id = (int)$row['new_value'];
$display_name = $display_map[$status_id] ?? null;
if (!$display_name) continue;
$ticket_data[] = [
'status' => $display_name,
'changed_at' => date('d-m-Y h:i A', strtotime($row['created_at'])),
];
}
$this->myLogger->logme('error', "[NON_EB_API][claimHistory] claim_id: $claim_id, steps: " . count($ticket_data) . ", user: {$authUser['id']}");
return $this->respond([
'status' => true,
'code' => 200,
'claim_id' => $claim_id,
'claims_data' => $claims_data,
'required_docs' => $required_docs,
'claims_files' => $claims_files,
'ticket_data' => $ticket_data,
], 200);
}
/**
* GET /api/v1/non-eb-claim/statuses
* Returns Non-EB claim statuses (ticket_type = 50).
*/
public function listClaimStatuses()
{
$authUser = $this->getAuthUser();
if (!$authUser) {
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
}
$statuses = $this->claimStatusModel
->select('id, claim_status, display_name')
->where('ticket_type', 50)
->where('is_active', 1)
->orderBy('id', 'ASC')
->findAll();
$this->myLogger->logme('error', "[NON_EB_API][listClaimStatuses] user: {$authUser['id']}, count: " . count($statuses));
return $this->respond([
'status' => true,
'code' => 200,
'data' => $statuses,
], 200);
}
/**
* POST /api/v1/non-eb-claim/policies
* Returns Non-EB / Marine policies for a given client (md5) + branch.
*
* Body:
* client_id string MD5 hash of the client's numeric id (required)
* client_branch_id int client branch id (required)
*/
public function listPolicies()
{
$authUser = $this->getAuthUser();
if (!$authUser) {
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
}
try {
$body = $this->request->getJSON(true) ?? $this->request->getPost();
} catch (\Throwable $e) {
return $this->respond(['status' => false, 'code' => 400, 'message' => 'Invalid JSON body'], 400);
}
$client_id_md5 = trim($body['client_id'] ?? '');
$client_branch_id = (int)($body['client_branch_id'] ?? 0);
$errors = [];
if (empty($client_id_md5)) {
$errors['client_id'] = 'client_id is required';
} elseif (!preg_match('/^[a-f0-9]{32}$/i', $client_id_md5)) {
$errors['client_id'] = 'client_id must be a valid MD5 hash';
}
if ($client_branch_id <= 0) {
$errors['client_branch_id'] = 'client_branch_id is required';
}
if (!empty($errors)) {
return $this->respond(['status' => false, 'code' => 400, 'message' => 'Input validation failed', 'errors' => $errors], 400);
}
$db = db_connect();
$builder = $db->table('client_policy cp');
$builder->select([
'cp.id',
'cp.policy_no',
'cp.policy_type_id',
'pt.policy_type AS policy_type_name',
'cp.insurer_id',
'i.name AS insurer_name',
'i.short_name AS insurer_short_name',
'DATE_FORMAT(cp.policy_start_date, "%d-%m-%Y") AS policy_start_date',
'DATE_FORMAT(cp.policy_end_date, "%d-%m-%Y") AS policy_end_date',
]);
$builder->join('policy_type pt', 'pt.id = cp.policy_type_id', 'left');
$builder->join('insurers i', 'i.id = cp.insurer_id AND i.is_active = 1', 'left');
$builder->where('MD5(cp.client_id)', $client_id_md5);
$builder->where('cp.client_branch_id', $client_branch_id);
$builder->where('cp.is_active', 1);
$builder->whereIn('pt.allocg', ['Non-EB', 'Marine']);
$builder->orderBy('cp.id', 'DESC');
$policies = $builder->get()->getResultArray();
$this->myLogger->logme('error', "[NON_EB_API][listPolicies] user: {$authUser['id']}, client_md5: $client_id_md5, branch: $client_branch_id, count: " . count($policies));
return $this->respond([
'status' => true,
'code' => 200,
'total' => count($policies),
'data' => $policies,
], 200);
}
public function uploadRequiredDoc()
{
$authUser = $this->getAuthUser();
if (!$authUser) {
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
}
$ticket_id = $this->request->getPost('ticket_id') ?? null;
$get_file_data = $this->request->getFiles('claim_docs') ?? null;
$get_docs_name = $this->request->getPost('claim_doc_names') ?? [];
$required_docs = $this->request->getPost('required_docs') ?? null;
if (is_string($get_docs_name)) {
$decoded = json_decode($get_docs_name, true);
$get_docs_name = json_last_error() === JSON_ERROR_NONE ? $decoded : [];
} elseif (!is_array($get_docs_name)) {
$get_docs_name = [];
}
$file_data = [];
if (!empty($get_file_data)) {
$file_path = WRITEPATH . 'uploads/claim_files/';
$file_data = storage_multi_file_Upload($get_file_data, $file_path, $get_docs_name, UPLOAD_EXT_CLAIM_DOCS);
}
if (!empty($file_data) && !empty($ticket_id)) {
foreach ($file_data as $value) {
$this->claimFilesModel->insert([
'ticket_id' => $ticket_id,
'ticket_type' => 2,
'doc_name' => $value['doc_name'],
'file_name' => $value['file_name'],
'url' => $value['file_path'],
'file_type' => 2,
'mime_type' => getMimeTypeByFileName($value['file_name']),
'docs_for_ir' => 1,
'is_active' => 1,
'created_by' => self::API_SYSTEM_USER_ID,
]);
}
db_connect()->query(
'UPDATE non_eb_ticket_master SET required_docs = ? WHERE id = ?',
[$required_docs, $ticket_id]
);
$this->myLogger->logme('error', "[NON_EB_API][uploadRequiredDoc] Docs uploaded. ticket_id: $ticket_id, count: " . count($file_data) . ", user: {$authUser['id']}");
return $this->respond(['status' => true, 'code' => 200, 'message' => 'Files uploaded successfully'], 200);
}
return $this->respond(['status' => false, 'code' => 400, 'message' => 'Failed to upload the file'], 400);
}
public function uploadRequiredDoc_v1(int $claim_id)
{
$authUser = $this->getAuthUser();
if (!$authUser) {
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
}
// Verify claim exists
$claim = $this->nonEbTicketModel
->select('id, client_id, required_docs')
->where('id', $claim_id)
->where('is_active', 1)
->first();
if (!$claim) {
return $this->respond(['status' => false, 'code' => 404, 'message' => 'Claim not found'], 404);
}
// Validate required_docs checklist is configured
$required_docs = json_decode($claim['required_docs'] ?? '{}', true);
if (empty($required_docs) || empty($required_docs['docs'])) {
return $this->respond(['status' => false, 'code' => 422, 'message' => 'No required documents checklist configured for this claim'], 422);
}
// Check if checklist is locked
if (!empty($required_docs['is_action_freeze'])) {
return $this->respond(['status' => false, 'code' => 423, 'message' => 'Document checklist is locked for this claim'], 423);
}
// Validate inputs
$document_name = trim($this->request->getPost('document_name') ?? '');
if (empty($document_name)) {
return $this->respond(['status' => false, 'code' => 400, 'message' => 'Input validation failed', 'errors' => ['document_name' => 'document_name is required']], 400);
}
$file = $this->request->getFile('file');
if (!$file || !$file->isValid() || $file->hasMoved()) {
return $this->respond(['status' => false, 'code' => 400, 'message' => 'Input validation failed', 'errors' => ['file' => 'A valid file is required']], 400);
}
// Validate file extension
$allowed = defined('UPLOAD_EXT_CLAIM_DOCS') ? UPLOAD_EXT_CLAIM_DOCS : ['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx', 'xls', 'xlsx'];
$ext = strtolower($file->getClientExtension());
if (is_array($allowed) && !in_array($ext, $allowed)) {
return $this->respond(['status' => false, 'code' => 415, 'message' => 'Unsupported file type: ' . $ext], 415);
}
// Find matching doc in checklist (exact case-sensitive match)
$matched_index = null;
foreach ($required_docs['docs'] as $i => $doc) {
if (($doc['document_name'] ?? '') === $document_name) {
$matched_index = $i;
break;
}
}
if ($matched_index === null) {
return $this->respond([
'status' => false, 'code' => 404,
'message' => "Document '{$document_name}' not found in required documents list",
], 404);
}
// Upload file and update required_docs atomically
$db = db_connect();
$db->transStart();
$upload_path = WRITEPATH . 'uploads/claim_files/';
$uploaded = storage_claim_file_Upload($file, $upload_path, UPLOAD_EXT_CLAIM_DOCS);
if ($uploaded === '') {
$db->transRollback();
return $this->respond(['status' => false, 'code' => 500, 'message' => 'File upload failed'], 500);
}
$disk_name = $uploaded['disk_name'];
$display_name = $uploaded['display_name'];
// Insert into claim_files
$this->claimFilesModel->insert([
'ticket_id' => $claim_id,
'ticket_type' => 2,
'doc_name' => $document_name,
'file_name' => $display_name,
'url' => $disk_name,
'file_type' => 2,
'mime_type' => getMimeTypeByFileName($display_name),
'is_active' => 1,
'created_by' => self::API_SYSTEM_USER_ID,
]);
// Mark document as received in required_docs JSON
$required_docs['docs'][$matched_index]['document_received'] = true;
$updated_json = json_encode($required_docs);
$db->query('UPDATE non_eb_ticket_master SET required_docs = ? WHERE id = ?', [$updated_json, $claim_id]);
$db->transComplete();
if (!$db->transStatus()) {
$this->myLogger->logme('error', "[NON_EB_API][uploadRequiredDoc] Transaction failed. claim_id: $claim_id, doc: $document_name, user: {$authUser['id']}");
return $this->respond(['status' => false, 'code' => 500, 'message' => 'Failed to save document. Please try again.'], 500);
}
$file_id = $this->claimFilesModel->insertID();
$download_url = base_url('downloadClaimFile/') . $file_id;
$this->myLogger->logme('error', "[NON_EB_API][uploadRequiredDoc] Doc uploaded. claim_id: $claim_id, doc: $document_name, file: $disk_name, user: {$authUser['id']}");
return $this->respond([
'status' => true,
'code' => 200,
'message' => 'Document uploaded successfully',
'claim_id' => $claim_id,
'document_name' => $document_name,
'download_url' => $download_url,
'required_docs' => $required_docs,
], 200);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,814 @@
<?php
// declare(strict_types=1);
namespace App\Controllers;
use Psr\Log\LoggerInterface;
use CodeIgniter\API\ResponseTrait;
use App\Models\AddImgModel;
use App\Models\FEContentModel;
use App\Models\ClientModel;
use App\Models\FAQModel;
class AppContentManagementController extends AdminController
{
use ResponseTrait;
protected $myLogger;
protected $addImgModel;
protected $feContentModel;
protected $clientModel;
protected $faqModel;
public function __construct()
{
$this->myLogger = \Config\Services::mylogger();
$this->addImgModel = new AddImgModel();
$this->feContentModel = new FEContentModel();
$this->clientModel = new ClientModel();
$this->faqModel = new FAQModel();
}
//listing
public function add_image_index()
{
$headerData['tab_name'] = 'Advertisement Images';
$headerData['page_name'] = 'Advertisement Images';
// $data['addImageList'] = $this->addImgModel->findAll();
$data['addImageList'] = $this->addImgModel->select('advertisement_images.*,
clients.client_name,
clients.short_name,
CASE WHEN advertisement_images.is_active = 1 THEN "Active" ELSE "Inactive" END AS status', false)
->join('clients', 'advertisement_images.client_id = clients.id', 'left')
->where('advertisement_images.is_active', 1)
->findAll();
$data['client'] = $this->clientModel->where('is_active', 1)->where('client_type', 1)->findAll();
// dd($data);
echo view('layout/header', $headerData);
echo view('add_image_list', $data);
echo view('layout/footer');
// $this->loadLayout('client_onboarding', $data);
}
// add and edit
public function add_advertise_image() {
try {
$data = $this->request->getPost();
$sanitized_post_data = sanitizeInputArrayAdvanced($data);
$id = (int)($sanitized_post_data['add_image_id'] ?? 0);
$rules = [
'add_image_id' => [
'rules' => 'permit_empty|integer|is_natural',
'errors' => [
'integer' => 'Image ID must be a valid number',
'is_natural' => 'Image ID must be a non-negative number'
]
],
'client_id' => [
'rules' => 'required|integer|is_natural',
'errors' => [
'required' => 'Client is required',
'integer' => 'Invalid client selected',
'is_natural' => 'Invalid client selected',
]
],
'advertise_image' => [
'rules' => ($id === 0 ? 'uploaded[advertise_image]|' : '')
. 'is_image[advertise_image]'
. '|mime_in[advertise_image,image/jpg,image/jpeg,image/png]'
. '|max_size[advertise_image,200]'
. '|max_dims[advertise_image,1640,664]',
'errors' => [
'uploaded' => 'Image is required',
'is_image' => 'File must be an image',
'mime_in' => 'Only JPG, JPEG, PNG allowed',
'max_size' => 'Image size must not exceed 200 KB',
'max_dims' => 'Image dimensions must not exceed 1640x664 pixels',
]
],
];
if (!$this->validate($rules)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false,
'message' => 'Input validation failed',
'code' => 400,
'errors' => $this->validator->getErrors()
]);
}
$file = $this->request->getFile('advertise_image');
$client_id = (int)($sanitized_post_data['client_id'] ?? 0);
if ($client_id !== 0) {
$clientExists = $this->clientModel->where('id', $client_id)->where('is_active', 1)->first();
if (!$clientExists) {
return $this->respond(['status' => false, 'message' => 'Invalid client selected.'], 400);
}
}
if (!$file || !$file->isValid()) {
return $this->respond(['status' => false, 'message' => 'No file uploaded or invalid file.'], 400);
}
if (!validate_upload_extension($file, UPLOAD_EXT_IMAGES)) {
return $this->respond(['status' => false, 'message' => 'Only JPG, JPEG, PNG files are allowed.'], 400);
}
$dimInfo = @getimagesize($file->getTempName());
if ($dimInfo === false || (int) $dimInfo[0] !== 1640 || (int) $dimInfo[1] !== 664) {
return $this->respond(['status' => false, 'message' => 'Image dimensions must be exactly 1640x664 pixels.'], 400);
}
$fileName = sanitize_upload_filename($file->getClientName());
$existing = $this->addImgModel->where('name', $fileName)->where('client_id', $client_id)->where('is_active', 1)->first();
if($existing){ return $this->respond(['status' => false, 'message' => 'This file has already been uploaded in active state.'], 400); }
$uploadPath = ROOTPATH . 'public/uploads/add_image_upload/';
if (!is_dir($uploadPath)) mkdir($uploadPath, 0755, true);
$file->move($uploadPath, $fileName);
$details = ['name' => $fileName,'client_id'=>$client_id];
if ($id === 0) {
$this->addImgModel->insert($details);
} else {
$existingRecord = $this->addImgModel->find($id);
if (!$existingRecord) {
return $this->respond(['status' => false, 'message' => 'Record not found for update.'], 404);
}
$this->addImgModel->update($id, $details);
}
return $this->respond(['status' => true, 'message' => 'Image saved successfully.']);
} catch (\Exception $e) {
return $this->respond(['status' => false, 'message' => $e->getMessage()], 500);
}
}
// soft delete
public function remove_advertise_image()
{
try {
$rules = [
'add_image_id' => [
'rules' => 'required|integer|is_natural_no_zero',
'errors' => [
'required' => 'Image ID is required',
'integer' => 'Image ID must be a valid number',
'is_natural_no_zero' => 'Image ID must be a positive number'
]
]
];
if (!$this->validate($rules)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false,
'message' => 'Input validation failed',
'code' => 400,
'errors' => $this->validator->getErrors()
]);
}
$id = (int)$this->request->getPost('add_image_id');
$existing = $this->addImgModel->find($id);
if (!$existing) {
return $this->respond(['status' => false, 'message' => 'Record not found'], 404);
}
$this->addImgModel->update($id, ['is_active' => 0]);
return $this->respond(['status' => true, 'message' => 'Deleted successfully']);
} catch (\Exception $e) {
return $this->respond(['status' => false, 'message' => $e->getMessage()], 500);
}
}
// Preview image Went Edit.
public function showAdvertiseImage($filename)
{
$filename = basename($filename);
if (!preg_match('/^[a-zA-Z0-9_\-]+\.(jpg|jpeg|png|gif|webp)$/i', $filename)) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
$path = ROOTPATH . 'public/uploads/add_image_upload/' . $filename;
$realPath = realpath($path);
$allowedDir = realpath(ROOTPATH . 'public/uploads/add_image_upload');
if (!$realPath || strpos($realPath, $allowedDir) !== 0 || !file_exists($realPath)) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
$mime = mime_content_type($realPath);
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mime, $allowedMimes, true)) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
return $this->response->setHeader('Content-Type', $mime)->setBody(file_get_contents($realPath));
}
public function frontend_content()
{
$method = $this->request->getMethod();
if ($this->request->getMethod() === 'post') {
/**
* --------------------------------------------------------------------------
* STEP 1: INITIAL VALIDATION
* --------------------------------------------------------------------------
* These are the basic validation rules. For 'content' and 'notes', we only
* check if they are provided and within the allowed length.
* The more advanced security check for script tags happens next.
*/
$rules = [
'fe_id' => [
'rules' => 'permit_empty|integer|is_natural',
'errors' => [
'integer' => 'ID must be a valid number',
'is_natural' => 'ID must be a non-negative number'
]
],
'type' => [
'rules' => 'required|max_length[255]|regex_match[/^[a-zA-Z0-9_ \-]+$/]',
'errors' => [
'required' => 'Type is required',
'max_length' => 'Type cannot exceed 255 characters',
'regex_match' => 'Type can contain only letters, numbers, spaces, _ and -'
]
],
'content_section' => [
'rules' => 'required|max_length[255]|regex_match[/^[a-zA-Z0-9_ \-]+$/]',
'errors' => [
'required' => 'Content Section is required',
'max_length' => 'Content Section cannot exceed 255 characters',
'regex_match' => 'Content Section can contain only letters, numbers, spaces, _ and -'
]
],
'heading' => [
'rules' => 'required|max_length[255]|regex_match[/^[a-zA-Z0-9 _\-.,;:!?()&\/]+$/]',
'errors' => [
'required' => 'Heading is required',
'max_length' => 'Heading cannot exceed 255 characters',
'regex_match' => 'Heading can contain only letters, numbers, spaces, and these characters: . , ; : ! ? ( ) & / -'
]
],
'content' => [
'rules' => 'required|max_length[5000]',
'errors' => [
'required' => 'Content is required',
'max_length' => 'Content cannot exceed 5000 characters',
]
],
'notes' => [
'rules' => 'required|max_length[1500]',
'errors' => [
'required' => 'Notes are required',
'max_length' => 'Notes cannot exceed 1500 characters',
]
],
];
if (!$this->validate($rules)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false,
'message' => 'Input validation failed',
'code' => 400,
'errors' => $this->validator->getErrors()
]);
}
/**************************************************************************
* REFACTORED SANITIZATION LOGIC (XSS Protection)
**************************************************************************
*
* Per the user's request, we are avoiding the generic `sanitizeInputArrayAdvanced`
* on the `content` and `notes` fields, as they require special HTML
* handling.
*
* The new process is:
* 1. Get the raw `content` and `notes` directly from the POST request.
* 2. Perform the critical XSS validation on this raw content using `hasXssTags()`.
* If it fails, the request is rejected immediately. This satisfies all
* the failure test cases (Tests 4-9).
* 3. Take all *other* POST data and sanitize it using the generic
* `sanitizeInputArrayAdvanced` function.
* 4. Sanitize the now-validated `content` and `notes` using our specific
* `sanitizeHtml()` function, which allows safe HTML.
* 5. Combine the sanitized data into a final array for database insertion.
*
*************************************************************************/
// Step 1: Get raw `content` and `notes`.
$rawContent = $this->request->getPost('content');
$rawNotes = $this->request->getPost('notes');
// Step 2: Perform critical XSS validation on raw input.
$xssErrors = [];
if ($this->hasXssTags($rawContent)) {
$xssErrors['content'] = 'Content contains restricted tags. Script, iframe and event handlers are not allowed';
}
if ($this->hasXssTags($rawNotes)) {
$xssErrors['notes'] = 'Notes contains restricted tags. Script, iframe and event handlers are not allowed';
}
if (!empty($xssErrors)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false,
'message' => 'Input validation failed',
'code' => 400,
'errors' => $xssErrors
]);
}
// Step 3: Sanitize all *other* POST data.
$otherPostData = $this->request->getPost();
unset($otherPostData['content'], $otherPostData['notes']);
$data = sanitizeInputArrayAdvanced($otherPostData);
// Step 4 & 5: Sanitize and re-combine `content` and `notes`.
$data['content'] = $this->sanitizeHtml($rawContent);
$data['notes'] = $this->sanitizeHtml($rawNotes);
$id = $data['fe_id'] ?? null;
if (!empty($id) && (!ctype_digit((string)$id) || (int)$id <= 0)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false, 'message' => 'Invalid ID format', 'code' => 400
]);
}
$id = !empty($id) ? (int)$id : null;
unset($data['fe_id']);
foreach ($data as $k => $v) {
if ($v === '' || $v === null) {unset($data[$k]);}
}
// INSERT / UPDATE
if (empty($id)) {
$status = $this->feContentModel->insert($data);
$text = "Created";
} else {
$existing = $this->feContentModel->find($id);
if (!$existing) {
return $this->response->setStatusCode(404)->setJSON([
'status' => false, 'message' => 'Record not found', 'code' => 404
]);
}
$status = $this->feContentModel->update($id, $data);
$text = "Updated";
}
return $this->respond([
'status' => $status ? true : false,
'message' => "Frontend Content $text " . ($status ? 'successfully' : 'failed'),
'code' => $status ? 200 : 400,
'data' => $data,
] , $status ? 200 : 400 );
}
elseif ($method === 'get') {
$id = $this->request->getGet('fe_id') ?? null;
if (!empty($id)) {
if (!ctype_digit((string)$id) || (int)$id <= 0) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false, 'code' => 400, 'message' => 'Invalid ID format'
]);
}
$id = (int)$id;
$data['fe_list'] = $this->feContentModel->where('id', $id)->orderBy('id', 'DESC')->findAll();
if (!empty($data)) {
return $this->respond(['status' => true, 'code' => 200, 'data' => $data], 200);
} else {
return $this->respond(['status' => false, 'code' => 400, 'message' => 'No data found'], 200);
}
}
$data['fe_list'] = $this->feContentModel->orderBy('id', 'DESC')->findAll();
return $this->loadLayout('frontend_content_list', ['data' => $data,'tab_name' => 'Front-End Content','page_name' => 'Front-End Content']);
} elseif ($method === 'delete') {
$id = $this->request->getGet('fe_id') ?? null;
if (empty($id) || !ctype_digit((string)$id) || (int)$id <= 0) {
return $this->respond([
'status' => false,
'code' => 400,
'message' => 'Valid numeric ID is required for deletion'
], 400);
}
$id = (int)$id;
$existing = $this->feContentModel->find($id);
if (!$existing) {
return $this->respond([
'status' => false,
'code' => 404,
'message' => 'Record not found'
], 404);
}
$update_status = $this->feContentModel->where('id', $id)->set(['is_active' => 0])->update();
if ($update_status) {
return $this->respond([
'status' => true,
'code' => 200,
'message' => 'Data removed successfully',
'fe_id' => $id
], 200);
} else {
return $this->respond([
'status' => false,
'code' => 400,
'message' => 'Failed to remove data',
'fe_id' => $id
], 200);
}
}
}
private const ALLOWED_RETURN_TYPES = ['api', 'web'];
public function FAQ()
{
$method = strtolower($this->request->getMethod());
$returnType = strtolower($this->request->getGet('return_type') ?? 'api');
if (!in_array($returnType, self::ALLOWED_RETURN_TYPES, true)) {
$returnType = 'api';
}
$ref = ['timestamp' => date('Y-m-d H:i:s')];
try {
// --- 1. POST: CREATE OR UPDATE ---
if ($method === 'post') {
$rules = [
'category' => [
// Allow letters, numbers, spaces and / - . , " '
'rules' => 'required|max_length[100]|regex_match[/^[a-zA-Z0-9 \\/\\-\\.\,\"\\\']+$/]',
'errors' => [
'required' => 'Category is required',
'max_length' => 'Category cannot exceed 100 characters',
'regex_match' => 'Category can contain only letters, numbers, spaces, and these characters: / - . , " \'',
]
],
'question' => [
// Allow only letters, numbers, spaces and basic punctuation . , ; : ! ? ( ) & / -
'rules' => 'required|max_length[1000]|regex_match[/^[a-zA-Z0-9 _\\-.,;:!?()&\\/]+$/]',
'errors' => [
'required' => 'Question is required',
'max_length' => 'Question cannot exceed 1000 characters',
'regex_match' => 'Question can contain only letters, numbers, spaces, and these characters: . , ; : ! ? ( ) & / -',
]
],
'answer' => [
'rules' => 'required|max_length[5000]',
'errors' => [
'required' => 'Answer is required',
'max_length' => 'Answer cannot exceed 5000 characters',
]
]
];
if (!$this->validate($rules)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => 'error',
'message' => 'Input validation failed',
'code' => 400,
'errors' => $this->validator->getErrors(),
'ref' => $ref
]);
}
/**************************************************************************
* XSS PROTECTION FOR 'question' and 'answer'
**************************************************************************
*
* Applying the same security model as `frontend_content`.
*
* 1. Validate raw `question` and `answer` for malicious tags using `hasXssTags()`.
* If found, reject the request immediately.
* 2. Sanitize all *other* fields using the generic `sanitizeInputArrayAdvanced`.
* 3. Sanitize the `question` and `answer` using the HTML-aware `sanitizeHtml()`
* function to allow safe tags before saving.
*
*************************************************************************/
// Step 1: Validate raw input for XSS threats.
$rawQuestion = $this->request->getPost('question');
$rawAnswer = $this->request->getPost('answer');
$xssErrors = [];
if ($this->hasXssTags($rawQuestion)) {
$xssErrors['question'] = 'Question contains restricted tags. Script, iframe and event handlers are not allowed';
}
if ($this->hasXssTags($rawAnswer)) {
$xssErrors['answer'] = 'Answer contains restricted tags. Script, iframe and event handlers are not allowed';
}
if (!empty($xssErrors)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => 'error',
'message' => 'Input validation failed',
'code' => 400,
'errors' => $xssErrors,
'ref' => $ref
]);
}
// Step 2 & 3: Sanitize and combine data.
$otherPostData = $this->request->getPost();
unset($otherPostData['question'], $otherPostData['answer']);
$data = sanitizeInputArrayAdvanced($otherPostData);
$data['question'] = $this->sanitizeHtml($rawQuestion);
$data['answer'] = $this->sanitizeHtml($rawAnswer);
$id = $data['faq_id'] ?? null;
if (!empty($id) && (!ctype_digit((string)$id) || (int)$id <= 0)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => 'error', 'message' => 'Invalid FAQ ID format', 'code' => 400, 'ref' => $ref
]);
}
$id = !empty($id) ? (int)$id : null;
unset($data['faq_id']);
if (empty($id)) {
$status = $this->faqModel->insert($data);
$msg = "Created";
} else {
$existing = $this->faqModel->find($id);
if (!$existing) {
return $this->response->setStatusCode(404)->setJSON([
'status' => 'error', 'message' => 'FAQ not found', 'code' => 404, 'ref' => $ref
]);
}
$status = $this->faqModel->update($id, $data);
$msg = "Updated";
}
return $this->response->setJSON([
'status' => $status ? 'success' : 'error',
'message' => "FAQ $msg " . ($status ? 'successfully' : 'failed'),
'code' => $status ? 200 : 400,
'data' => $data,
'ref' => $ref
])->setStatusCode($status ? 200 : 400);
}
// --- 2. GET: FETCH LIST OR SINGLE ---
elseif ($method === 'get') {
$id = $this->request->getGet('faq_id');
if (!empty($id)) {
if (!ctype_digit((string)$id) || (int)$id <= 0) {
return $this->response->setStatusCode(400)->setJSON([
'status' => 'error', 'message' => 'Invalid FAQ ID format', 'code' => 400, 'ref' => $ref
]);
}
$id = (int)$id;
if($returnType === 'web'){
$row = $this->faqModel->find($id);
}else{
$row = $this->faqModel->where('is_active', 1)->find($id);
}
$data['faq_list'] = $row ? [$row] : [];
} else {
if($returnType === 'web'){
$data['faq_list'] = $this->faqModel->orderBy('id', 'desc')->findAll();
}else{
$data['faq_list'] = $this->faqModel->where('is_active', 1)->orderBy('id', 'desc')->findAll();
}
}
if ($returnType === 'web') {
$data['tab_name'] = "FAQ";
$data['page_name'] = "FAQ";
// print_r($data);die;
return $this->loadLayout('faq_list', $data);
}
// API Response Logic
if (empty($data['faq_list'])) {
return $this->response->setJSON([
'status' => $returnType === 'web' ? false : 'error',
'message' => 'No data found',
'code' => 404,
'data' => [],
'ref' => $ref
])->setStatusCode(200); // Using 200 with error status is common for mobile apps to prevent crashes
}
return $this->response->setJSON([
'status' => $returnType === 'web' ? true : 'success',
'message' => 'Data retrieved',
'code' => 200,
'data' => $data,
'ref' => $ref
])->setStatusCode(200);
}
// --- 3. DELETE: SOFT DELETE ---
elseif ($method === 'delete') {
$id = $this->request->getGet('faq_id');
if (empty($id) || !ctype_digit((string)$id) || (int)$id <= 0) {
return $this->response->setJSON([
'status' => 'error', 'message' => 'Valid numeric FAQ ID is required', 'code' => 400, 'ref' => $ref
])->setStatusCode(400);
}
$id = (int)$id;
$status = $this->faqModel->find($id) ? $this->faqModel->update($id, ['is_active' => 0]) : false;
return $this->response->setJSON([
'status' => $status ? ($returnType === 'web' ? true : 'success') : ($returnType === 'web' ? false : 'error'),
'message' => $status ? 'Data removed successfully' : 'Failed to remove or ID missing',
'code' => $status ? 200 : 400,
'data' => [],
'ref' => $ref
])->setStatusCode( 200 );
}
} catch (\Throwable $e) {
log_message('error', 'FAQ error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
return $this->response->setJSON([
'status' => $returnType === 'web' ? false : 'error',
'code' => 500,
'message' => 'An internal error occurred. Please try again later.',
'ref' => $ref
])->setStatusCode(500);
}
}
// public function frontend_content_files()
// {
// // 1. Define your physical system path
// define('UPLOAD_PATH', ROOTPATH . 'public/uploads/add_image_upload/');
// // 2. Define the public URL so the editor can display the image later
// // This is what the browser uses to see the image (e.g., https://site.com/uploads/...)
// // $publicUrl = "https://yourdomain.com/public/uploads/add_image_upload/";
// $publicUrl = ROOTPATH . 'public/uploads/add_image_upload/';
// // if (!is_dir($uploadPath)) mkdir($uploadPath, 0755, true);
// header('Content-Type: application/json');
// if ($_FILES['files']) {
// // Ensure directory exists
// if (!is_dir(UPLOAD_PATH)) {
// mkdir(UPLOAD_PATH, 0775, true);
// }
// $fileName = time() . '_' . basename($_FILES['files']['name'][0]);
// $fullPath = UPLOAD_PATH . $fileName;
// if (move_uploaded_file($_FILES['files']['tmp_name'][0], $fullPath)) {
// echo json_encode([
// "success" => true,
// "data" => [
// "baseurl" => $publicUrl,
// "files" => [$fileName]
// ]
// ]);
// } else {
// echo json_encode(["success" => false, "error" => "Failed to move file to " . UPLOAD_PATH]);
// }
// }
// }
// Add these two private methods inside AppContentManagementController
/**
* =================================================================================
* HTML SANITIZATION & VALIDATION HELPER METHODS
* =================================================================================
* The following two methods are the core of the XSS protection logic.
*/
/**
* sanitizeHtml()
*
* This function cleans a string of HTML, ensuring it is safe to display in a browser.
* It allows a specific set of safe HTML tags and removes any dangerous attributes
* from those tags.
*
* @param string $input The raw HTML string from user input.
* @return string The cleaned, safe HTML string.
*/
private function sanitizeHtml(string $input): string
{
/**
* Define a whitelist of allowed HTML tags. Any tag not in this list will be
* completely removed. We are allowing basic formatting, lists, tables, etc.
*/
// ✅ Added <s>, <u>, <h1>-<h6>, <blockquote>, <pre>, <code>, <hr> for Jodit support
$allowed_tags = '<p><b><i><s><u><strong><em><ul><ol><li><br><a><img><table><thead><tbody><tr><th><td><span><div><h1><h2><h3><h4><h5><h6><blockquote><pre><code><hr><sub><sup>';
// Use strip_tags() to remove all tags that are not in our whitelist.
$clean = strip_tags($input, $allowed_tags);
/**
* Define a blacklist of dangerous attributes. These are often used for XSS
* attacks (e.g., `onclick`, `onmouseover`). We search for and remove these
* attributes from any remaining tags.
*/
$dangerous_attrs = [
'/\s*on\w+\s*=\s*["\'][^"\']*["\']/i', // e.g., onclick="..."
'/\s*on\w+\s*=\s*[^\s>]*/i', // e.g., onclick=...
'/\s*javascript\s*:[^"\'"]*/i', // e.g., href="javascript:..."
'/\s*vbscript\s*:[^"\'"]*/i', // e.g., href="vbscript:..."
];
// Use preg_replace to find and remove the dangerous attributes.
foreach ($dangerous_attrs as $pattern) {
$clean = preg_replace($pattern, '', $clean);
}
return $clean;
}
/**
* hasXssTags()
*
* This function scans a string for common XSS-related tags, protocols, and event
* handlers. It is used as a primary check to quickly reject any input that is
* clearly malicious.
*
* @param string $str The raw string from user input.
* @return bool Returns `true` if a dangerous pattern is found, `false` otherwise.
*/
private function hasXssTags(string $str): bool
{
// Decode the string to handle entities (e.g., `%3Cscript%3E`) and prevent evasion.
$decoded = html_entity_decode($str, ENT_QUOTES, 'UTF-8');
$decoded = urldecode($decoded);
$decoded = str_replace(["\0", "\x00"], '', $decoded); // Remove null bytes
/**
* Define a blacklist of dangerous patterns. This includes tags like `<script>`
* and `<iframe>`, as well as patterns like `javascript:` and `onclick=`.
* The `/i` flag makes the search case-insensitive.
*/
$dangerous_patterns = [
'/<\s*script/i', // <script
'/<\s*\/\s*script/i', // </script
'/javascript\s*:/i', // javascript:
'/vbscript\s*:/i', // vbscript:
'/<\s*iframe/i', // <iframe>
'/<\s*object/i', // <object>
'/<\s*embed/i', // <embed>
'/<\s*applet/i', // <applet>
'/on\w+\s*=/i', // on...= (e.g., onclick=, onmouseover=, onerror=)
'/data\s*:\s*text\/html/i', // data:text/html
'/expression\s*\(/i', // CSS expression()
'/\balert\s*\(/i', // alert(...)
];
// Loop through the patterns and check if any of them exist in the decoded string.
foreach ($dangerous_patterns as $pattern) {
if (preg_match($pattern, $decoded)) return true; // Found a threat
}
// If we get here, no threats were found.
return false;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,68 @@
<?php
namespace App\Controllers;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\CLIRequest;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
/**
* Class BaseController
*
* BaseController provides a convenient place for loading components
* and performing functions that are needed by all your controllers.
* Extend this class in any new controllers:
* class Home extends BaseController
*
* For security be sure to declare any new methods as protected or private.
*/
abstract class BaseController extends Controller
{
/**
* Instance of the main Request object.
*
* @var CLIRequest|IncomingRequest
*/
protected $request;
/**
* An array of helpers to be loaded automatically upon
* class instantiation. These helpers will be available
* to all other controllers that extend BaseController.
*
* @var array
*/
protected $helpers = [];
/**
* Be sure to declare properties for any property fetch you initialized.
* The creation of dynamic property is deprecated in PHP 8.2.
*/
// protected $session;
/**
* @return void
*/
protected $session;
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
{
// Do Not Edit This Line
// echo 'initController';
parent::initController($request, $response, $logger);
// Preload any models, libraries, etc, here.
$this->session = \Config\Services::session();
}
public function loadLayout($view_name,$data = [])
{
echo view('layout/header',$data);
echo view($view_name, $data);
echo view('layout/footer',$data);
}
}

View File

@ -0,0 +1,452 @@
<?php
namespace App\Controllers;
use App\Helpers\MailHelper;
use App\Helpers\sendMailNotification;
use App\Models\EmployeeModel;
use App\Models\EmployeePolicyModel;
use App\Models\ClientModel;
use App\Models\ClientRMModel;
use App\Models\PolicesModel;
use App\Models\RelationshipModel;
use App\Models\FileModel;
use App\Models\ClientPolicyModel;
use App\Models\PolicyPremium1Model;
use App\Models\PolicyPremium2Model;
use App\Models\PolicyTypeModel;
use App\Models\NotificationModel;
use App\Models\UserModel;
use App\Models\FEContentModel;
use App\Models\AddImgModel;
use App\Models\ChatBotModel;
use CodeIgniter\API\ResponseTrait;
class ChatBotController extends AdminController
{
use ResponseTrait;
protected $myLogger;
protected $employeeModel;
protected $employeePolicyModel;
protected $clientModel;
protected $clientRMModel;
protected $fileModel;
protected $policesModel;
protected $relationshipModel;
protected $clientPolicyModel;
protected $policyPremium1Model;
protected $policyPremium2Model;
protected $policyTypeModel;
protected $notificationModel;
protected $userModel;
protected $feContentModel;
protected $addImgModel;
protected $ChatBotModel;
public function __construct()
{
set_session_context('Employee');
$this->myLogger = \Config\Services::mylogger();
$this->employeeModel = new EmployeeModel();
$this->employeePolicyModel = new EmployeePolicyModel();
$this->clientModel = new ClientModel();
$this->clientRMModel = new ClientRMModel();
$this->policesModel = new PolicesModel();
$this->relationshipModel = new RelationshipModel();
$this->fileModel= new FileModel();
$this->clientPolicyModel = new ClientPolicyModel();
$this->policyPremium1Model = new PolicyPremium1Model();
$this->policyPremium2Model = new PolicyPremium2Model();
$this->policyTypeModel = new PolicyTypeModel();
$this->notificationModel = new NotificationModel();
$this->userModel = new UserModel();
$this->feContentModel = new FEContentModel();
$this->addImgModel = new AddImgModel();
$this->ChatBotModel = new ChatBotModel();
}
public function getChatResponse()
{
try {
$emp_code = $this->request->getVar('emp_code');
$client_id = $this->request->getVar('client_id');
$client_branch_id = $this->request->getVar('client_branch_id');
$request_for = $this->request->getVar('request_for');
$option = $this->request->getVar('option');
$is_option = $this->request->getVar('is_option');
$responseData = $this->ChatBotModel->where('request', $request_for)
->where('is_active', 1 )
->first();
if ($responseData) {
if(isset($option) && $option != 0)
{
$decodedData = json_decode($responseData['options'],true);
if (isset($decodedData[$option]) && $decodedData[$option] !== null) {
$requestFor = $decodedData[$option];
$responseData = $this->ChatBotModel->where('request', $requestFor)
->where('is_active', 1 )
->first();
if($responseData['request'] == 'Call' || $responseData['request'] == 'Mail'){
$AccountManagerDetails = $this->clientRMModel->select('client_rm.* , user_profiles.*')
->join('user_profiles', 'client_rm.user_id = user_profiles.id', 'left')
->where('client_rm.client_id', $client_id )
->where('client_rm.level', 3 )
->findAll();
if(isset($AccountManagerDetails) && $responseData['request'] == 'Call'){
$responseData['options'] = $AccountManagerDetails[0]['mobile'];
}else if(isset($AccountManagerDetails) && $responseData['request'] == 'Mail'){
$responseData['options'] = $AccountManagerDetails[0]['email'];
}
}
}else{
if( $request_for == 'Initiate claim'){
$result = [
'request_for' => $request_for,
'options' => json_decode($responseData['options'], true),
'text' => "Enter Message",
'is_option' => "0",
'policy_id' => $option,
'mobile_no' => $this->request->getVar('mobile_no'),
'navigation' => $responseData['navigation'],
'navigation_type' => $responseData['navigation_type']
];
return $this->respond(['status' => 'success','code' => 200,'data' => $result ],200);
}else{
$result = $this->sendDefaultMessage();
return $this->respond(['status' => 'success','code' => 200,'data' => $result ],200);
}
}
}
if(isset($is_option) && $is_option == 0){
$text = $this->request->getVar('text');
$value = $this->request->getVar('value');
$Data = $this->ChatBotModel->where('options', $text)->where('is_active', 1 )->first();
if($Data){
if($Data['options'] == 'Please Enter Your Mobile Number To View Your Policies')
{
$result = $this->getAllPolicyByMobileNumber($value,'text');
return $this->respond(['status' => 'success','code' => 200,'data' => $result ],200);
}else if($Data['options'] == 'Please Enter Your Mobile Number To Raise Claim'){
$result = $this->getAllPolicyByMobileNumber($value,'options');
return $this->respond(['status' => 'success','code' => 200,'data' => $result ],200);
}else if( $Data['options'] == 'Enter Your Claim Number'){
$result = $this->getTicket($value,$emp_code,$client_id);
return $this->respond(['status' => 'success','code' => 200,'data' => $result ],200);
}
}else{
if( $request_for == 'Create claim' && $text == 'Enter Message'){
$ticket = $this->claimTicket();
$ticket = json_decode($ticket,true);
if (isset($ticket['success']) && $ticket['success'] == 1) {
$result = [
'request_for' => $request_for,
'options' => null,
'text' => $ticket['message'].', Your Claim id is '.$ticket['ticket_id'],
'is_option' => "0",
'navigation' => "0",
'navigation_type' => null
];
}else{
$result = [
'request_for' => $request_for,
'options' => null,
'text' => $ticket['message'],
'is_option' => "0",
'navigation' => "0",
'navigation_type' => null
];
}
return $this->respond(['status' => 'success','code' => 200,'data' => $result ],200);
}else{
$result = $this->sendDefaultMessage();
return $this->respond(['status' => 'success','code' => 200,'data' => $result ],200);
}
}
}
$result = [
'request_for' => $responseData['request'],
'options' => json_decode($responseData['options'], true),
'text' => $responseData['is_option'] != 1 ? $responseData['options'] : null,
'is_option' => $responseData['is_option'],
'navigation' => $responseData['navigation'],
'navigation_type' => $responseData['navigation_type']
];
return $this->respond(['status' => 'success','code' => 200,'data' => $result ],200);
} else {
return $this->respond(['status' => 'failed','code' => 404,'data' => 'No Data'],404);
}
} catch (\Throwable $th) {
return $this->respond(['status' => 'failed','code' => 500,'data' => $th],500);
}
}
public function sendDefaultMessage()
{
$responseData = $this->ChatBotModel->where('id', 1)->where('is_active', 1 )->first();
return [
'request_for' => $responseData['request'],
'options' => json_decode($responseData['options'], true),
'text' => $responseData['is_option'] != 1 ? $responseData['options'] : null,
'is_option' => $responseData['is_option'],
'navigation' => $responseData['navigation'],
'navigation_type' => $responseData['navigation_type']
];
}
public function getAllPolicyByMobileNumber($mobileNo,$action)
{
$employee = $this->employeeModel->where('mobile', $mobileNo)->where('is_active', 1 )->first();
if($employee){
$employeeData = $this->employeeModel->where('emp_code', $employee['emp_code'])
->where('client_id',$employee['client_id'])
->where('client_branch_id',$employee['client_branch_id'])
->where('is_active', 1 )->findAll();
$whereArrayForId = [];
foreach ( $employeeData as $key => $value) { array_push($whereArrayForId, $value['id']); }
$ClientPolicyData = $this->clientPolicyModel->select('client_policy.* , policy_type.long_name as policy_name , policy_type.policy_type as policy_type')
->join('policy_type', 'client_policy.policy_type_id = policy_type.id', 'left')
->where('client_policy.client_id', $employee['client_id'] )
->where('client_policy.client_branch_id', $employee['client_branch_id'] )
->where('client_policy.is_active', 1 )
->where('client_policy.policy_status', 1)
->orderby('client_policy.id' , 'ASC')
->findAll();
$options = [];
$text = '';
if(count($ClientPolicyData) > 0 && count($employeeData) > 0)
{
foreach ($ClientPolicyData as $key => $ClientPolicyValue) {
$data['policy_name'] = $ClientPolicyValue['policy_name'];
$data['policy_type'] = $ClientPolicyValue['policy_type'];
$data['policy_no'] = $ClientPolicyValue['policy_no'];
$employee_policy = $this->employeePolicyModel->select('employees.*,employee_polices.employee_id , employee_polices.basic_cover_si , employee_polices.premium , employee_polices.gst , employee_polices.tpa_id , employee_polices.rand_string , employee_polices.uhid as uhid')
->join('employees', 'employee_polices.employee_id = employees.id', 'left')
->whereIn('employee_polices.employee_id',$whereArrayForId)
->where('employee_polices.client_policy_id',$ClientPolicyValue['id'])
->where('employee_polices.is_active', 1 )->findAll();
if(count($employee_policy) > 0)
{
$policy[$ClientPolicyValue['id']] = $ClientPolicyValue['policy_name']."(".$ClientPolicyValue['policy_type'].")";
array_push($options, $policy);
$text .= $ClientPolicyValue['policy_name']." % ";
}else{
$text .= "There is no any active policies ";
}
}
if($action == 'options'){
return [
'request_for' => $this->request->getVar('request_for'),
'options' => $options[count($options) - 1],
'text' => null,
'is_option' => "1",
'mobile_no' => $mobileNo,
];
}else{
return [
'request_for' => $this->request->getVar('request_for'),
'options' => null,
'text' => $text,
'is_option' => "0",
'mobile_no' => $mobileNo,
];
}
}else{
return [
'request_for' => $this->request->getVar('request_for'),
'options' => null,
'text' => "Dear ".$employee['name']." Policy not yet bound for you.",
'is_option' => "0"
];
}
}else{
return [
'request_for' => $this->request->getVar('request_for'),
'options' => null,
'text' => "Invalid Mobile Number",
'is_option' => "0"
];
}
}
public function claimTicket()
{
$employee = $this->employeeModel->where('mobile', $this->request->getVar('mobile_no'))->where('is_active', 1 )->first();
if($employee){
$url = 'https://venbait.in/nhance/helpdesk/dev/api/tickets/create';
$token = 'uncp8FvG310bEyYdV9MmStlo7KDRZ65fLWTeXCI2JzwPrNHjBqQhUiAgxsaO';
$headers = [
'Token: ' . $token,
];
// Form data
$data = [
'opener' => 'user',
'department_id' => 2,
'subject' => 'Claim Raised by Chat Bot',
'body' => $this->request->getVar('value'),
'policy_id' => $this->request->getVar('policy_id'),
'emp_code' => $employee['emp_code'],
'mobile_number' => $this->request->getVar('mobile_no'),
'email' => 'adhavanvalli@gmail.com',
'fullname' => $employee['name'],
];
// Initialize cURL session
$ch = curl_init();
// Set cURL options
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
return $response;
// Check for errors
// if (curl_errno($ch)) {
// $error = curl_error($ch);
// curl_close($ch);
// return ['status' => 'failed', 'response' => $error];
// } else {
// // Close cURL session
// curl_close($ch);
// return ['status' => 'success', 'response' => $response];
// }
}else{
return ['status' => 'failed', 'response' => 'Employee Not Found'];
}
}
public function getTicket($value, $emp_code, $client_id)
{
// Construct the URL with query parameters
$url = 'https://venbait.in/nhance/helpdesk/dev/api/tickets/show/' . $value;
$token = 'uncp8FvG310bEyYdV9MmStlo7KDRZ65fLWTeXCI2JzwPrNHjBqQhUiAgxsaO';
$headers = [
'Token: ' . $token,
];
// Initialize cURL session
$ch = curl_init();
// Set cURL options
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// Execute cURL request
$response = curl_exec($ch);
// Check for errors
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
return ['status' => 'failed', 'response' => $error];
} else {
// Close cURL session
curl_close($ch);
return ['status' => 'success', 'response' => $response];
}
}
}

View File

@ -0,0 +1,129 @@
<?php
// namespace App\Conversations;
namespace App\Controllers\Chatbot;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use App\Helpers\ChatbotHelper;
class EcardDownloadConversation extends Conversation
{
protected $buttonsData = [];
public function run()
{
$this->bot->types();
// sleep(0.5);
$this->showEcardMenu();
}
protected function showEcardMenu()
{
// Log entry for debugging
log_message('error', 'showEcardMenu function called');
// Get chat session and policies
$chat_session_info = get_chatbot_session_info();
$policy_list = ChatbotHelper::getListOfPolicies($chat_session_info);
// If policies found, build an HTML list of links (and raw URLs as fallback)
if (is_array($policy_list) && count($policy_list)) {
$parts = [];
foreach ($policy_list as $policy) {
// Resolve rand string (defensive)
$randString = '';
if (isset($policy['rand_string']) && $policy['rand_string'] !== '') {
$randString = $policy['rand_string'];
} elseif (isset($policy['rand']) && $policy['rand'] !== '') {
$randString = $policy['rand'];
} elseif (isset($policy['emp_policy_id'])) {
// As a last resort, use emp_policy_id (not ideal but prevents broken links)
$randString = $policy['emp_policy_id'];
}
// Build link; make sure base_url produces correct path
$link = base_url('/download-e-card/' . $randString . '/1');
// Escape policy name to avoid HTML injection
$safeName = isset($policy['policy_name']) ? htmlspecialchars($policy['policy_name'], ENT_QUOTES, 'UTF-8') : 'Policy';
// Add to parts; include anchor and raw URL fallback
$parts[] = "🔹 <a href=\"{$link}\" target=\"_blank\">{$safeName}</a><br>";
}
$message = "Choose a policy to download (click the policy name below):<br><br>" . implode('<br><br>', $parts);
log_message('error', 'Ecard links displayed: ' . json_encode(array_column($policy_list, 'policy_name')));
$this->say($message);
} else {
log_message('error', 'No policies found for ecard download');
$this->say('No policy found.');
}
// After showing links, move to confirmation conversation (Do you want to continue?)
// This keeps the UX identical to previous flow where we asked the user if they want to continue.
$this->bot->startConversation(new doYouWantToContinueConversation());
}
protected function showEcardMenuOLD()
{
log_message('error', ('showEcardMenu function called'));
$chat_session_info = get_chatbot_session_info();
$policy_list = ChatbotHelper::getListOfPolicies($chat_session_info);
$buttons = [];
$question = 'Choose Policy to Download Ecard:';
log_message('error', ('policy_list : ' . json_encode($policy_list)));
if(is_array($policy_list) && count($policy_list))
{
foreach($policy_list as $policy)
{
$this->buttonsData[$policy['emp_policy_id'].'#'.$policy['rand_string']] = ['response_text' => "🔹 {$policy['policy_name']}"] ;
$buttons[] = Button::create("{$policy['policy_name']}")->value($policy['emp_policy_id'].'#'.$policy['rand_string']);
}
}
else
{
$question = 'No policy found';
}
$this->buttonsData['go_back'] = ['response_text' => '◀️ Go Back'] ;
$buttons = array_merge($buttons,[Button::create("◀️ Go Back")->value("go_back")]);
$question = Question::create($question)
->addButtons($buttons);
$temp = $this->buttonsData;
$this->bot->ask($question, function ($answer) use ($temp) {
log_message('error', 'showEcardMenu answer received');
log_message('error', ($answer));
$selectedOption = $answer->getText();
$this->say("You have selected {$temp[$selectedOption]['response_text']}");
switch ($answer->getValue()) {
case "go_back":
$this->bot->startConversation(new MainMenuConversation());
break;
case is_string($answer->getValue()) && is_array(explode('#',$answer->getValue())) && count((explode('#',$answer->getValue()))) == 2:
$link = base_url().'/download-e-card/' . explode('#',$answer->getValue())[1].'/1';
log_message('error', $link);
$this->say('Click here to downlad: <a href="'.$link.'" target="_blank">GMC</a><br>Click here to downlad: <a href="'.$link.'" target="_blank">GMC Parent</a>');
$this->bot->startConversation(new doYouWantToContinueConversation()); // ✅ Restart the
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->bot->startConversation(new EcardDownloadConversation()); // ✅ Restart the conversation
break;
}
});
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Controllers\Chatbot;
use App\Helpers\ChatbotHelper;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use BotMan\Drivers\Web\WebDriver;
class LoginToContiueConversation extends Conversation
{
public function run()
{
$this->bot->userStorage()->delete();
$this->bot->types(); // Typing indicator for the first message
sleep(0.5); // Delay
$this->showLoginMessage();
}
protected function showLoginMessage()
{
$this->say("Kindly Login to use the chatbot");
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace App\Controllers\Chatbot;
use App\Helpers\ChatbotHelper;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use BotMan\Drivers\Web\WebDriver;
class MainMenuConversation extends Conversation
{
protected $buttonsData = [
"ecard_download" => ["response_text" => "📄 Ecard Download"],
"network_hospital" => ["response_text" => "🏥 Network Hospital"],
"reimbursement_claim" => ["response_text" => "💰 Reimbursement Claim Process"],
"reimbursement_status" => ["response_text" => "📑 Reimbursement Claim Status"],
"new_policy" => ["response_text" => "🆕 New Policy"],
"renew_policy" => ["response_text" => "🔄 Renew Policy"],
];
public function run()
{
$this->bot->userStorage()->delete();
$this->bot->types(); // Typing indicator for the first message
// sleep(0.5); // Delay
$this->showMainMenu();
}
protected function showMainMenu()
{
$buttons = [];
foreach ($this->buttonsData as $key => $data) {
$buttons[] = Button::create($data['response_text'])->value($key);
}
$this->bot->userStorage()->save([
'path' => []
]);
$question = Question::create("Please choose an option:")->addButtons($buttons);
$temp = $this->buttonsData;
$this->bot->ask($question, function ($answer) use( $temp ){
$user_reponse = $answer->getValue();
if (isset($temp[$user_reponse])) {
$message = "You've chosen " . $temp[$user_reponse]['response_text'];
// $this->bot->say($message,,WebDriver::class);
$this->bot->reply($message);
}
log_message('error', ('user_reponse before: ' . $user_reponse));
if (!$answer->isInteractiveMessageReply()) {
$user_reponse = null;
}
log_message('error', ('user_reponse is interactive: ' . $answer->isInteractiveMessageReply()));
// Get just the existing path array
$path = $this->bot->userStorage()->get('path') ?? [];
// Add new value
array_push($path, $answer->getValue());
// Save ONLY the path back
$this->bot->userStorage()->save([
'path' => $path
]);
log_message('error', ('user_reponse after: ' . $user_reponse));
switch ($user_reponse) {
case "ecard_download":
$this->bot->startConversation(new EcardDownloadConversation());
log_message('error', 'ecard_download clicked ');
break;
case "network_hospital":
$this->bot->startConversation(new NetworkHospitalConversation());
log_message('error', 'network_hospital clicked ');
break;
case "reimbursement_claim":
$this->bot->startConversation(new ReimbursementClaimProcessConversation());
log_message('error', 'reimbursement_claim clicked ');
break;
case "reimbursement_status":
$this->bot->startConversation(new ReimbursementClaimStatusConversation());
log_message('error', 'reimbursement_status clicked ');
break;
case "new_policy":
case "renew_policy":
$this->bot->startConversation(new policyConversation());
log_message('error', 'renew_policy || new_policy clicked ');
break;
default:
log_message('error', 'default shown ');
$this->say("Invalid selection. Please choose an option.");
$this->bot->startConversation(new MainMenuConversation());
break;
}
});
}
}

View File

@ -0,0 +1,145 @@
<?php
// namespace App\Conversations;
namespace App\Controllers\Chatbot;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use App\Helpers\ChatbotHelper;
class NetworkHospitalConversation extends Conversation
{
public function run()
{
$this->bot->types(); // Typing indicator for the first message
// sleep(0.5); // Delay
$this->showHospitalMenu();
}
protected function showHospitalMenuOLD()
{
$chat_session_info = get_chatbot_session_info();
$policy_list = ChatbotHelper::getListOfPolicies($chat_session_info);
$buttons = [];
$question = 'Choose Policy:';
if(is_array($policy_list) && count($policy_list))
{
foreach($policy_list as $policy)
{
// $question = Question::create("Choose Policy to Download Ecard:")
// ->addButtons([
// Button::create("🔹 $policy['policy_name']")->value("$policy['emp_policy_id']"),
// Button::create("◀️ Go Back")->value("go_back"),
// ]);
$buttons[] = Button::create("🔹 {$policy['policy_name']}")->value($policy['emp_policy_id'].'#'.$policy['rand_string']);
}
}
else
{
$question = 'No policy found';
}
$buttons = array_merge($buttons,[Button::create("◀️ Go Back")->value("go_back")]);
$question = Question::create($question)
->addButtons($buttons);
// $this->bot->hears('.*', function ($bot) {
// $this->bot->types(); // Typing indicator for the first message
// sleep(0.5); // Delay
// });
$this->bot->ask($question, function ($answer) {
$path = $this->bot->userStorage()->get('path');
array_push($path,
$answer->getValue()
);
$this->bot->userStorage()->save([
'path' => $path
]);
// log_message('error', ('user_reponse : ' . $answer->getValue()));
// log_message('error', ('user_reponse : ' . json_encode(explode('#',$answer->getValue()))));
switch ($answer->getValue()) {
case is_string($answer->getValue()) && is_array(explode('#',$answer->getValue())) && count((explode('#',$answer->getValue()))) == 2:
$emp_poilicy_id = explode('#',$answer->getValue());
$emp_poilicy_id = array_shift($emp_poilicy_id);
log_message('error','EMP POL ID FOR HOSPITAL' . $emp_poilicy_id);
$hospital_link = ChatbotHelper::getHospitalLink($emp_poilicy_id);
if($hospital_link)
{
$this->say('<a href="' . $hospital_link . '" target="_blank">Click here for hospital details</a>');
}
else
{
$this->say('No Data found, please contact support team');
}
$this->bot->startConversation(new doYouWantToContinueConversation());
break;
case "go_back":
$this->bot->startConversation(new MainMenuConversation());
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->bot->startConversation(new NetworkHospitalConversation());
break;
}
});
}
protected function showHospitalMenu()
{
log_message('error', 'showHospitalMenu function called');
// Get chat session and policies
$chat_session_info = get_chatbot_session_info();
$policy_list = ChatbotHelper::getListOfPolicies($chat_session_info);
if (is_array($policy_list) && count($policy_list)) {
$parts = [];
foreach ($policy_list as $policy) {
// Resolve rand string (defensive)
$randString = '';
if (isset($policy['rand_string']) && $policy['rand_string'] !== '') {
$randString = $policy['rand_string'];
} elseif (isset($policy['rand']) && $policy['rand'] !== '') {
$randString = $policy['rand'];
} elseif (isset($policy['emp_policy_id'])) {
$randString = $policy['emp_policy_id'];
}
$emp_policy_id = isset($policy['emp_policy_id']) ? $policy['emp_policy_id'] : '';
$hospital_link = ChatbotHelper::getHospitalLink($emp_policy_id);
$safeName = isset($policy['policy_name']) ? htmlspecialchars($policy['policy_name'], ENT_QUOTES, 'UTF-8') : 'Policy';
if ($hospital_link) {
$parts[] = "🔹 <a href=\"{$hospital_link}\" target=\"_blank\">{$safeName}</a><br>";
} else {
$parts[] = "🔹 {$safeName} — No Data found, please contact support team";
}
log_message('error', 'Hospital link for policy ' . $emp_policy_id . ': ' . $hospital_link);
}
$message = "Access hospital details for your policies below:<br><br>" . implode('<br><br>', $parts);
$this->say($message);
} else {
log_message('error', 'No policies found for hospital menu');
$this->say('No policy found.');
}
// After showing links, move to confirmation conversation
$this->bot->startConversation(new doYouWantToContinueConversation());
}
}

View File

@ -0,0 +1,37 @@
<?php
// namespace App\Conversations;
namespace App\Controllers\Chatbot;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use App\Helpers\ChatbotHelper;
class ReimbursementClaimProcessConversation extends Conversation
{
public function run()
{
$this->bot->types(); // Typing indicator for the first message
sleep(0.5); // Delay
$this->claimProcess();
}
protected function claimProcess()
{
$chat_session_info = get_chatbot_session_info();
$res = ChatbotHelper::sendReimbursementProcessOverMail($chat_session_info);
if($res == true)
{
$this->say("Reimbursement process shared over your registered mail...!");
}
else
{
$this->say("No email found in your profile / Please contact support team");
}
$this->bot->startConversation(new doYouWantToContinueConversation());
}
}

View File

@ -0,0 +1,47 @@
<?php
// namespace App\Conversations;
namespace App\Controllers\Chatbot;
use App\Helpers\ChatbotHelper;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
class ReimbursementClaimStatusConversation extends Conversation
{
public function run()
{
$this->claimStatus();
}
protected function claimStatus()
{
$this->say("Fetching Status please wait ...");
$chat_session_info = get_chatbot_session_info();
$data = ChatbotHelper::getReimbursementClaimStatus($chat_session_info);
log_message('error','Claim Number - ' . json_encode($data['claim_number']));
$this->bot->types();
if ($data){
if(isset($data['claim_number']) && $data['claim_number'] !== null)
{
log_message('error','Claim Number YES- ' . json_encode($data['claim_number']));
$this->say("The claim status for the claim Number {$data['claim_number']} is currently in <strong>{$data['client_status']}</strong> status. <br> More Detailed Information is sent to your mail");
}
else
{
log_message('error','Claim Number NO- ' . json_encode($data['claim_number']));
$this->say("The claim status for your latest claim (ID not generated) is currently in <strong>{$data['client_status']}</strong> status. <br> More Detailed Information is sent to your mail");
}
$this->bot->startConversation(new doYouWantToContinueConversation());
}else{
$this->say("No Claim Raised against the user.");
$this->bot->startConversation(new doYouWantToContinueConversation());
}
}
}

View File

@ -0,0 +1,77 @@
<?php
// namespace App\Conversations;
namespace App\Controllers\Chatbot;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use App\Helpers\ChatbotHelper;
class commonPolicyOptionConversations extends Conversation
{
protected $buttonsData = [
"upload_vehicle_details" => ["response_text" => "🔹 Upload Vehicle Details"],
"service_executive" => ["response_text" => "🔹 Talk to our Service Executive"],
"go_back" => ["response_text" => "◀️ Go Back"],
];
public function run()
{
$this->policyOptions();
}
protected function policyOptions()
{
$buttons = [];
foreach ($this->buttonsData as $key => $data) {
$buttons[] = Button::create($data['response_text'])->value($key);
}
// $this->bot->userStorage()->save([
// 'path' => []
// ]);
$question = Question::create("Please choose an option:")->addButtons($buttons);
$temp = $this->buttonsData;
$this->bot->ask($question, function ($answer) use( $temp ){
$user_reponse = $answer->getValue();
if (isset($temp[$user_reponse])) {
$message = "You've chosen " . $temp[$user_reponse]['response_text'];
// $this->bot->say($message,,WebDriver::class);
$this->bot->reply($message);
}
if (!$answer->isInteractiveMessageReply()) {
$user_reponse = null;
}
$path = $this->bot->userStorage()->get('path');
array_push($path,
$answer->getValue()
);
$this->bot->userStorage()->save([
'path' => $path
]);
$path = $this->bot->userStorage()->get('path');
// $this->say(json_encode($path));
switch ($user_reponse) {
case "upload_vehicle_details":
// $cityName = $response->getText();
$this->bot->startConversation(new vehicleFormConversation());
break;
case "service_executive":
$this->bot->startConversation(new serviceExecutiveConversation());
break;
case "go_back":
$this->bot->startConversation(new MainMenuConversation());
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->policyOptions();
break;
}
});
}
}

View File

@ -0,0 +1,90 @@
<?php
// namespace App\Conversations;
namespace App\Controllers\Chatbot;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use App\Helpers\ChatbotHelper;
class doYouWantToContinueConversation extends Conversation
{
protected $buttonsData = [
"yes" => ["response_text" => "🔹 Yes I need some more Help"],
"no" => ["response_text" => "🔹 No, Thank You"]
];
public function run()
{
$this->bot->types(); // Typing indicator for the first message
sleep(3); // Delay
$this->askQuestion();
}
protected function askQuestion()
{
$buttons = [];
foreach ($this->buttonsData as $key => $data) {
$buttons[] = Button::create($data['response_text'])->value($key);
}
// $this->bot->userStorage()->save([
// 'path' => []
// ]);
$question = Question::create("Please choose an option:")->addButtons($buttons);
$temp = $this->buttonsData;
$this->bot->ask($question, function ($answer) use( $temp ){
$user_reponse = $answer->getValue();
if (isset($temp[$user_reponse])) {
$message = "You've chosen " . $temp[$user_reponse]['response_text'];
// $this->bot->say($message,,WebDriver::class);
$this->bot->reply($message);
}
if (!$answer->isInteractiveMessageReply()) {
$user_reponse = null;
}
$path = $this->bot->userStorage()->get('path');
array_push($path,
$answer->getValue()
);
$this->bot->userStorage()->save([
'path' => $path
]);
switch ($user_reponse) {
case "yes":
// $this->say("Sure, How can I help you ");
$this->bot->startConversation(new MainMenuConversation());
break;
case "no":
// $user = $this->bot->getUser();
// $userID = $user->getId();
$dataBefore = $this->bot->userStorage()->all();
// log_message('info','Before Deletion: ' . json_encode($dataBefore));
$this->bot->userStorage()->delete();
$this->bot->userStorage()->save([]);
// Cache::flush();
$dataAfter = $this->bot->userStorage()->all();
// log_message('info','After Deletion: ' . json_encode($dataAfter));
$this->say("Thank you for your time! If you need anything else, feel free to reach out. Goodbye!");
$this->say("Say Hi or Help or Hello to start conversation again!");
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->askQuestion();
break;
}
});
}
}

View File

@ -0,0 +1,75 @@
<?php
// namespace App\Conversations;
namespace App\Controllers\Chatbot;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use App\Helpers\ChatbotHelper;
class fourWheelerOptionsConversations extends Conversation
{
protected $buttonsData = [
"comp" => ["response_text" => "🔹 Comprehensive"],
"3p" => ["response_text" => "🔹 Third-Party Only"],
"go_back" => ["response_text" => "◀️ Go Back"],
];
public function run()
{
$this->show4WOptions();
}
protected function show4WOptions()
{
$buttons = [];
foreach ($this->buttonsData as $key => $data) {
$buttons[] = Button::create($data['response_text'])->value($key);
}
// $this->bot->userStorage()->save([
// 'path' => []
// ]);
$question = Question::create("Please choose an option:")->addButtons($buttons);
$temp = $this->buttonsData;
$this->bot->ask($question, function ($answer) use( $temp ){
$user_reponse = $answer->getValue();
if (isset($temp[$user_reponse])) {
$message = "You've chosen " . $temp[$user_reponse]['response_text'];
// $this->bot->say($message,,WebDriver::class);
$this->bot->reply($message);
}
if (!$answer->isInteractiveMessageReply()) {
$user_reponse = null;
}
$path = $this->bot->userStorage()->get('path');
array_push($path,
$answer->getValue()
);
$this->bot->userStorage()->save([
'path' => $path
]);
$path = $this->bot->userStorage()->get('path');
// $this->say(json_encode($path));
switch ($user_reponse) {
case "3p":
// $cityName = $response->getText();
$this->bot->startConversation(new commonPolicyOptionConversations());
break;
case "comp":
$this->bot->startConversation(new commonPolicyOptionConversations());
break;
case "go_back":
$this->bot->startConversation(new MainMenuConversation());
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->showHospitalMenu();
break;
}
});
}
}

View File

@ -0,0 +1,72 @@
<?php
// namespace App\Conversations;
namespace App\Controllers\Chatbot;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use App\Helpers\ChatbotHelper;
class hospitalPolicyConversation extends Conversation
{
protected $buttonsData = [
"individual" => ["response_text" => "🔹 Individual"],
"floater" => ["response_text" => "🔹 Floater"],
"go_back" => ["response_text" => "◀️ Go Back"],
];
public function run()
{
$this->showMediclaimPolicyOptions();
}
protected function showMediclaimPolicyOptions()
{
$path = $this->bot->userStorage()->get('path');
if (in_array('individual',$path)){
$text = 'Upload Policy Copy and Individual Details';
$value = 'individual_upload';
}else{
$text = 'Upload Policy Copy and Family Details';
$value = 'family_upload';
}
$question = Question::create("Choose Policy Type:")
->addButtons([
Button::create("🔹 ".$text)->value($value),
Button::create("🔹 Talk to our Service Executive")->value("service_executive"),
Button::create("◀️ Go Back")->value("go_back"),
]);
$this->bot->ask($question, function ($answer) use ($value, $text){
$path = $this->bot->userStorage()->get('path');
array_push($path,
$answer->getValue()
);
$this->bot->userStorage()->save([
'path' => $path
]);
switch ($answer->getValue()) {
case $value:
$this->say("you have chosen ".$text);
$this->bot->startConversation(new medicalClaimFormConversations());
break;
case "service_executive":
$this->say('You have chosen to speak with our Service Executive');
$this->bot->startConversation(new serviceExecutiveConversation());
break;
case "go_back":
$this->bot->startConversation(new medicalClaimOptionConversations());
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->showHospitalMenu();
break;
}
});
}
}

View File

@ -0,0 +1,317 @@
<?php
namespace App\Controllers\Chatbot;
use App\Helpers\ChatbotHelper;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use BotMan\BotMan\Messages\Conversations\Conversation;
class medicalClaimFormConversations extends Conversation
{
private $currentMemberData = [
'name' => null,
'dob' => null,
'si' => null,
'pre_existing_disease' => null
];
public function run()
{
// Initialize state if not exists
if (!$this->bot->userStorage()->get('claim_form_state')) {
$this->bot->userStorage()->save([
'claim_form_state' => [
'dependency_count' => 0,
'current_member' => 'primary',
'dependencies' => [],
'primary_insurer' => null
]
]);
}
$state = $this->bot->userStorage()->get('claim_form_state');
$memberType = ($state['current_member'] === 'primary') ? 'primary insured person' : 'dependency';
$this->askName($memberType);
}
private function askName($memberType)
{
$this->ask("Enter the Name of the $memberType?", function($answer) {
$name = $answer->getText();
if (empty($name)) {
$this->say('Name Cannot be Empty');
return $this->run();
}
// Get existing state and update only the name
$state = $this->bot->userStorage()->get('claim_form_state');
if ($state['current_member'] === 'primary') {
if (!isset($state['primary_insurer'])) {
$state['primary_insurer'] = [];
}
$state['primary_insurer']['name'] = $name;
} else {
if (!isset($state['temp_dependency'])) {
$state['temp_dependency'] = [];
}
$state['temp_dependency']['name'] = $name;
}
$this->bot->userStorage()->save(['claim_form_state' => $state]);
$this->askDOB();
});
}
private function askDOB()
{
$this->ask('Enter Date of Birth (DD/MM/YYYY)?', function($answer) {
$dob = $answer->getText();
if (empty($dob)) {
$this->say('Date of Birth Cannot be Empty');
return $this->askDOB();
}
// Get existing state and update only the DOB
$state = $this->bot->userStorage()->get('claim_form_state');
if ($state['current_member'] === 'primary') {
$state['primary_insurer']['dob'] = $dob;
} else {
$state['temp_dependency']['dob'] = $dob;
}
$this->bot->userStorage()->save(['claim_form_state' => $state]);
$this->askSumInsured();
});
}
private function askSumInsured()
{
$this->ask('Enter Sum Insured Amount?', function($answer) {
$sum_insured = $answer->getText();
if (empty($sum_insured)) {
$this->say('Sum Insured Cannot be Empty');
return $this->askSumInsured();
}
// Get existing state and update only the sum insured
$state = $this->bot->userStorage()->get('claim_form_state');
if ($state['current_member'] === 'primary') {
$state['primary_insurer']['si'] = $sum_insured;
} else {
$state['temp_dependency']['si'] = $sum_insured;
}
$this->bot->userStorage()->save(['claim_form_state' => $state]);
$this->askPreExistingDisease();
});
}
private function askPreExistingDisease()
{
$this->ask('Enter Pre-Existing Disease if any (type "none" if none)?', function($answer) {
$pre_existing_disease = $answer->getText();
if (empty($pre_existing_disease)) {
$this->say('Please enter none if no pre-existing conditions');
return $this->askPreExistingDisease();
}
// Get existing state and update the disease info
$state = $this->bot->userStorage()->get('claim_form_state');
if ($state['current_member'] === 'primary') {
$state['primary_insurer']['pre_existing_disease'] = $pre_existing_disease;
$state['primary_insurer']['type'] = 'primary';
} else {
$state['temp_dependency']['pre_existing_disease'] = $pre_existing_disease;
$state['temp_dependency']['type'] = 'dependency';
// Move completed dependency to dependencies array
if (!isset($state['dependencies'])) {
$state['dependencies'] = [];
}
$state['dependencies'][] = $state['temp_dependency'];
$state['temp_dependency'] = null; // Clear temporary dependency
}
$this->bot->userStorage()->save(['claim_form_state' => $state]);
// For debugging
log_message('debug', 'Final state after all data collection: ' . json_encode($state));
$this->saveMemberAndContinue();
});
}
private function saveMemberAndContinue()
{
$state = $this->bot->userStorage()->get('claim_form_state');
$memberData = $state['current_member'] === 'primary'
? $state['primary_insurer']
: end($state['dependencies']);
// Show current member info
$memberInfo = "👤 **" . ucfirst($memberData['type']) . " Member Details:**\n";
$memberInfo .= "🔹 Name: " . $memberData['name'] . "\n";
$memberInfo .= "🔹 Date of Birth: " . $memberData['dob'] . "\n";
$memberInfo .= "🔹 Sum Insured: " . $memberData['si'] . "\n";
$memberInfo .= "🔹 Pre Existing Disease: " . $memberData['pre_existing_disease'] . "\n";
$this->say($memberInfo);
$path = $this->bot->userStorage()->get('path');
if (in_array('floater', $path)) {
$this->askAboutDependencies();
} else {
$this->showSummary();
}
}
private function askAboutDependencies()
{
$question = Question::create("Do you want to add a dependency? ")
->addButtons([
Button::create("✅ Yes")->value("yes"),
Button::create("❌ No")->value("no"),
Button::create("◀️ Go Back")->value("go_back"),
]);
$this->ask($question, function($answer) {
$state = $this->bot->userStorage()->get('claim_form_state');
switch ($answer->getValue()) {
case "yes":
$state['dependency_count']++;
$state['current_member'] = 'dependency';
$this->bot->userStorage()->save(['claim_form_state' => $state]);
// Reset currentMemberData for new dependency
$this->currentMemberData = [
'name' => null,
'dob' => null,
'si' => null,
'pre_existing_disease' => null
];
$this->run();
break;
case "no":
$this->showSummary();
break;
case "go_back":
$this->bot->startConversation(new hospitalPolicyConversation());
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->askAboutDependencies();
break;
}
});
}
private function showSummary()
{
$state = $this->bot->userStorage()->get('claim_form_state');
$finalSummary = '<div style="font-family: Arial, sans-serif; font-size: 15px; margin: 0; padding: 2px;">';
// Primary Insurer
if ($state['primary_insurer']) {
$finalSummary .= '<div style="margin: 2px 0; padding-left: 5px;">Primary:<br>' .
$this->formatSingleLineMemberInfo($state['primary_insurer']) .
'</div>';
}
// Dependencies
if (!empty($state['dependencies'])) {
foreach ($state['dependencies'] as $index => $dependency) {
$finalSummary .= '<div style="margin: 2px 0; padding-left: 5px;">Dep ' . ($index + 1) . ':<br>' .
$this->formatSingleLineMemberInfo($dependency) .
'</div>';
}
}
$finalSummary .= '</div>';
$this->say($finalSummary);
$question = Question::create("Confirm all details:")
->addButtons([
Button::create("✅ Confirm")->value("confirm"),
Button::create("🔁 Start Over")->value("restart"),
Button::create("❌ Cancel")->value('cancel')
]);
$this->ask($question, function ($answer) use ($finalSummary) {
switch ($answer->getValue()) {
case "confirm":
$sent_status = ChatbotHelper::sendQuotationRequestMail($finalSummary);
if ($sent_status) {
$this->say('Our Executive will contact you with the quotation soon.');
$this->bot->startConversation(new doYouWantToContinueConversation());
} else {
$this->say("There was a problem while requesting for a quotation. Please try again later.");
}
break;
case "restart":
$this->bot->userStorage()->save([
'claim_form_state' => [
'dependency_count' => 0,
'current_member' => 'primary',
'dependencies' => [],
'primary_insurer' => null
]
]);
$this->run();
break;
case "cancel":
$this->bot->startConversation(new MainMenuConversation());
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->showSummary();
break;
}
});
}
private function formatSingleLineMemberInfo($member)
{
return '🔹 Name: ' . htmlspecialchars($member['name']) . '<br>' .
'🔹 DOB: ' . htmlspecialchars($member['dob']) . '<br>' .
'🔹 SI: ' . htmlspecialchars($member['si']) . '<br>' .
'🔹 PED: ' . htmlspecialchars($member['pre_existing_disease']);
}
// private function formatMemberInfo($member)
// {
// // Format each piece of information with proper HTML
// return '<div style="line-height: 1.6;">
// <div style="margin-bottom: 5px;">
// <span style="color: #2c3e50;">🔹 Name:</span>
// <span style="color: #34495e; font-weight: 500;">' . htmlspecialchars($member['name']) . '</span>
// </div>
// <div style="margin-bottom: 5px;">
// <span style="color: #2c3e50;">🔹 DOB:</span>
// <span style="color: #34495e; font-weight: 500;">' . htmlspecialchars($member['dob']) . '</span>
// </div>
// <div style="margin-bottom: 5px;">
// <span style="color: #2c3e50;">🔹 Sum Insured:</span>
// <span style="color: #34495e; font-weight: 500;">' . htmlspecialchars($member['si']) . '</span>
// </div>
// <div style="margin-bottom: 5px;">
// <span style="color: #2c3e50;">🔹 Pre-Existing Conditions:</span>
// <span style="color: #34495e; font-weight: 500;">' . htmlspecialchars($member['pre_existing_disease']) . '</span>
// </div>
// </div>';
// }
}

View File

@ -0,0 +1,75 @@
<?php
// namespace App\Conversations;
namespace App\Controllers\Chatbot;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use App\Helpers\ChatbotHelper;
class medicalClaimOptionConversations extends Conversation
{
protected $buttonsData = [
"individual" => ["response_text" => "🔹 Individual"],
"floater" => ["response_text" => "🔹 Floater"],
"go_back" => ["response_text" => "◀️ Go Back"],
];
public function run()
{
$this->showMediclaimOptions();
}
protected function showMediclaimOptions()
{
$buttons = [];
foreach ($this->buttonsData as $key => $data) {
$buttons[] = Button::create($data['response_text'])->value($key);
}
// $this->bot->userStorage()->save([
// 'path' => []
// ]);
$question = Question::create("Please choose an option:")->addButtons($buttons);
$temp = $this->buttonsData;
$this->bot->ask($question, function ($answer) use( $temp ){
$user_reponse = $answer->getValue();
if (isset($temp[$user_reponse])) {
$message = "You've chosen " . $temp[$user_reponse]['response_text'];
// $this->bot->say($message,,WebDriver::class);
$this->bot->reply($message);
}
if (!$answer->isInteractiveMessageReply()) {
$user_reponse = null;
}
$path = $this->bot->userStorage()->get('path');
array_push($path,
$answer->getValue()
);
$this->bot->userStorage()->save([
'path' => $path
]);
switch ($user_reponse) {
case "individual":
// $cityName = $response->getText();
$this->bot->startConversation(new hospitalPolicyConversation());
break;
case "floater":
$this->bot->startConversation(new hospitalPolicyConversation());
break;
case "go_back":
$this->bot->startConversation(new MainMenuConversation());
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->showHospitalMenu();
break;
}
});
}
}

View File

@ -0,0 +1,82 @@
<?php
// namespace App\Conversations;
namespace App\Controllers\Chatbot;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use App\Helpers\ChatbotHelper;
class policyConversation extends Conversation
{
protected $buttonsData = [
"2_wheeler" => ["response_text" => "🏍️ 2 Wheeler"],
"4_wheeler" => ["response_text" => "🚗 4 Wheeler"],
"mediclaim" => ["response_text" => "🩺 Medical Claim"],
"go_back" => ["response_text" => "◀️ Go Back"],
];
public function run()
{
$this->showNewPolicy();
}
protected function showNewPolicy()
{
$buttons = [];
foreach ($this->buttonsData as $key => $data) {
$buttons[] = Button::create($data['response_text'])->value($key);
}
// $this->bot->userStorage()->save([
// 'path' => []
// ]);
$question = Question::create("Please choose an option:")->addButtons($buttons);
$temp = $this->buttonsData;
$this->bot->ask($question, function ($answer) use( $temp ){
$user_reponse = $answer->getValue();
if (isset($temp[$user_reponse])) {
$message = "You've chosen " . $temp[$user_reponse]['response_text'];
// $this->bot->say($message,,WebDriver::class);
$this->bot->reply($message);
}
if (!$answer->isInteractiveMessageReply()) {
$user_reponse = null;
}
log_message('error', ('user_reponse : ' . $answer->getValue()));
$path = $this->bot->userStorage()->get('path');
array_push($path,
$answer->getValue()
);
$this->bot->userStorage()->save([
'path' => $path
]);
switch ($user_reponse) {
case "2_wheeler":
// $cityName = $response->getText();
$this->bot->startConversation(new twoWheelerOptionsConversations());
break;
case "4_wheeler":
$this->bot->startConversation(new fourWheelerOptionsConversations());
break;
case "mediclaim":
$this->bot->startConversation(new medicalClaimOptionConversations());
break;
case "go_back":
$this->bot->startConversation(new MainMenuConversation());
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->showHospitalMenu();
break;
}
});
}
}

View File

@ -0,0 +1,93 @@
<?php
// namespace App\Conversations;
namespace App\Controllers\Chatbot;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use App\Helpers\ChatbotHelper;
class selectInsurerConversations extends Conversation
{
public function run()
{
$this->insurerOptions();
}
protected function insurerOptions()
{
$question = Question::create("Choose Your Options:")
->addButtons([
Button::create("🔹 Select Insurers")->value("insurers"),
Button::create("🔹 Talk to our Service Executive")->value("service_executive"),
Button::create("◀️ Go Back")->value("go_back"),
]);
$this->bot->ask($question, function ($answer) {
$path = $this->bot->userStorage()->get('path');
array_push($path,
$answer->getValue()
);
$this->bot->userStorage()->save([
'path' => $path
]);
switch ($answer->getValue()) {
case "insurers":
$data = ChatbotHelper::getInsurersList();
log_message("error",'data: '.json_encode($data));
$question = Question::create("Select an Insurer:");
$page = $this->bot->userStorage()->get('insurer_page') ?? 0;
$perPage = 5;
$totalPages = ceil(count($data) / $perPage);
$insurersToShow = array_slice($data, $page * $perPage, $perPage);
$buttons = [];
foreach ($insurersToShow as $insurer) {
$buttons[] = Button::create("🔹 " . $insurer['short_name'])->value($insurer['short_name']);
}
// Add pagination buttons
if ($page > 0) {
$buttons[] = Button::create("⬅️ Previous")->value("previous_page");
}
if (($page + 1) < $totalPages) {
$buttons[] = Button::create("➡️ Next")->value("next_page");
}
$question = Question::create("Choose an Insurer:")
->addButtons($buttons);
$this->bot->ask($question, function ($response) {
$selected = $response->getValue();
if ($selected == "previous_page") {
$this->bot->userStorage()->save(["insurer_page" => max(0, $this->bot->userStorage()->get('insurer_page') - 1)]);
$this->insurerOptions();
} elseif ($selected == "next_page") {
$this->bot->userStorage()->save(["insurer_page" => $this->bot->userStorage()->get('insurer_page') + 1]);
$this->insurerOptions();
} else {
$this->say("You selected: " . $selected);
}
});
break;
case "service_executive":
$this->bot->startConversation(new serviceExecutiveConversation());
break;
case "go_back":
$this->bot->startConversation(new MainMenuConversation());
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->policyOptions();
break;
}
});
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Controllers\Chatbot;
use App\Helpers\ChatbotHelper;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
class serviceExecutiveConversation extends Conversation
{
protected $mobile_number;
public function run()
{
$this->askServiceExecutive();
}
protected function askServiceExecutive()
{
$chat_session_info = get_chatbot_session_info();
$mobile = ChatbotHelper::getPhoneNumber($chat_session_info); // Assuming this fetches the user's phone number
$question = Question::create("Is this Your Mobile Number: $mobile")
->addButtons([
Button::create("✅ Yes")->value("yes"),
Button::create("❌ No")->value("no"),
Button::create("◀️ Go Back")->value("go_back"),
]);
$this->bot->ask($question, function ($answer) {
$path = $this->bot->userStorage()->get('path');
array_push($path,
$answer->getValue()
);
$this->bot->userStorage()->save([
'path' => $path
]);
switch ($answer->getValue()) {
case "yes":
$this->say("Our executive will call you at the earliest.");
$this->bot->startConversation(new doYouWantToContinueConversation());
break;
case "no":
$this->ask('Enter Your Mobile Number?', function ($response) {
$this->mobile_number = $response->getText();
$this->bot->userStorage()->save([
'mobile_number' => $this->mobile_number
]);
// Use 'use' to bring the class context into the closure
$this->say("Our executive will call you at the earliest.");
$this->bot->startConversation(new doYouWantToContinueConversation());
});
break;
case "go_back":
$this->bot->startConversation(new MainMenuConversation());
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->askServiceExecutive();
break;
}
});
}
}

View File

@ -0,0 +1,75 @@
<?php
// namespace App\Conversations;
namespace App\Controllers\Chatbot;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use App\Helpers\ChatbotHelper;
class twoWheelerOptionsConversations extends Conversation
{
protected $buttonsData = [
"3p" => ["response_text" => "🔹 Third-Party"],
"comp" => ["response_text" => "🔹 Comprehensive"],
"go_back" => ["response_text" => "◀️ Go Back"]
];
public function run()
{
$this->show2WOptions();
}
protected function show2WOptions()
{
$buttons = [];
foreach ($this->buttonsData as $key => $data) {
$buttons[] = Button::create($data['response_text'])->value($key);
}
// $this->bot->userStorage()->save([
// 'path' => []
// ]);
$question = Question::create("Please choose an option:")->addButtons($buttons);
$temp = $this->buttonsData;
$this->bot->ask($question, function ($answer) use( $temp ){
$user_reponse = $answer->getValue();
if (isset($temp[$user_reponse])) {
$message = "You've chosen " . $temp[$user_reponse]['response_text'];
// $this->bot->say($message,,WebDriver::class);
$this->bot->reply($message);
}
if (!$answer->isInteractiveMessageReply()) {
$user_reponse = null;
}
$path = $this->bot->userStorage()->get('path');
array_push($path,
$answer->getValue()
);
$this->bot->userStorage()->save([
'path' => $path
]);
switch ($user_reponse) {
case "3p":
// $cityName = $response->getText();
$this->bot->startConversation(new commonPolicyOptionConversations());
break;
case "comp":
$this->bot->startConversation(new commonPolicyOptionConversations());
break;
case "go_back":
$this->bot->startConversation(new MainMenuConversation());
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->showHospitalMenu();
break;
}
});
}
}

View File

@ -0,0 +1,219 @@
<?php
namespace App\Controllers\Chatbot;
use App\Helpers\ChatbotHelper;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use BotMan\BotMan\Messages\Conversations\Conversation;
class vehicleFormConversation extends Conversation
{
protected $vehicleName;
protected $vehicleYOM;
protected $vehicleEngineNo;
protected $vehicleInvoiceNo;
protected $userId;
protected $complied_information;
protected $fuelType;
protected $seater;
protected $chassisNumber;
public function run()
{
// Ensure $this->bot is properly initialized
// $user = $this->bot->getUser();
// $this->userId = $user->getId(); // Correctly store user ID
$this->askVehicleName(); // Start asking vehicle questions
}
protected function askVehicleName()
{
$this->ask('Enter Vehicle Name ?', function ($response) {
$this->vehicleName = $response->getText();
if (empty($this->vehicleName)) {
$this->say('Vehicle Name Cannot be Empty');
return $this->askVehicleName(); // Re-ask if empty
}
// $this->askVehicleYOM(); // Ask next question
$path = $this->bot->userStorage()->get('path');
if (in_array('4_wheeler', $path)) {
$this->askFuelType();
} else {
$this->askVehicleYOM(); // Ask next question
}
});
}
protected function askFuelType()
{
$this->ask('Enter Fuel Type of the Vehicle ?', function ($response) {
$this->fuelType = $response->getText();
if (empty($this->fuelType)) {
$this->say('Vehicle Fuel Type Cannot be Empty');
return $this->askFuelType(); // Re-ask if empty
}
$this->askSeating(); // Ask next question
});
}
protected function askSeating()
{
$this->ask('Enter the seat count of the Vehicle ?', function ($response) {
$this->seater = $response->getText();
if (empty($this->seater)) {
$this->say('Vehicle seat Cannot be Empty');
return $this->askSeating(); // Re-ask if empty
}
$this->askVehicleYOM(); // Ask next question
});
}
protected function askVehicleYOM()
{
$this->ask('Enter Vehicle Year of Manufacture ?', function ($response) {
$this->vehicleYOM = $response->getText();
if (empty($this->vehicleYOM)) {
$this->say('Vehicle Year of Manufacture Cannot be Empty');
return $this->askVehicleYOM(); // Re-ask if empty
}
$this->askVehicleEngineNo(); // Ask next question
});
}
protected function askVehicleEngineNo()
{
$this->ask('Enter Vehicle Engine Number ?', function ($response) {
$this->vehicleEngineNo = $response->getText();
if (empty($this->vehicleEngineNo)) {
$this->say('Vehicle Engine Number Cannot be Empty');
return $this->askVehicleEngineNo(); // Re-ask if empty
}
// Retrieve 'path' from storage
$path = $this->bot->userStorage()->get('path');
if (in_array('4_wheeler', $path)) {
$this->askChassisNumber();
} else {
$this->askVehicleInvoiceNo(); // Ask next question
}
// $this->askVehicleInvoiceNo();
});
}
protected function askChassisNumber()
{
$this->ask('Enter Chassis Number of the Vehicle ?', function ($response) {
$this->chassisNumber = $response->getText();
if (empty($this->chassisNumber)) {
$this->say('Vehicle Chassis Number Cannot be Empty');
return $this->askChassisNumber(); // Re-ask if empty
}
$this->askVehicleInvoiceNo(); // Ask next question
});
}
protected function askVehicleInvoiceNo()
{
$this->ask('Enter Vehicle Invoice Number ?', function ($response) {
$this->vehicleInvoiceNo = $response->getText();
if (empty($this->vehicleInvoiceNo)) {
$this->say('Vehicle Invoice Number Cannot be Empty');
return $this->askVehicleInvoiceNo(); // Re-ask if empty
}
$this->storeVehicleData(); // Proceed to store the data
});
}
protected function storeVehicleData()
{
// Retrieve 'path' from storage
$path = $this->bot->userStorage()->get('path');
// $this->say(json_encode($path));
// Prepare vehicle details
$vehicleDetails = [
'vehicle_name' => $this->vehicleName,
'vehicle_yom' => $this->vehicleYOM,
'vehicle_engine_no' => $this->vehicleEngineNo,
$vehicleDetails['vehicle_invoice_no'] = $this->vehicleInvoiceNo
];
// Only store 'vehicle_invoice_no' if 'new_policy' is NOT in path
if (in_array('4_wheeler', $path)) {
$vehicleDetails['vehicle_fuelType'] = $this->fuelType;
$vehicleDetails['vehicle_seater'] = $this->seater;
$vehicleDetails['vehicle_chassisNo'] = $this->chassisNumber;
}
// Save vehicle details
$this->bot->userStorage()->save(['vehicle_details' => $vehicleDetails]);
$alldata = $this->bot->userStorage()->all();
$vehicle_details = $this->bot->userStorage()->get('vehicle_details');
// Log stored data for debugging
// log_message('error', 'All Details: ' . json_encode($alldata));
// log_message('error', 'Updated Vehicle Details: ' . json_encode($vehicle_details));
$this->say('Vehicle Information Saved!');
$vehicleInfo = "🚗 **Vehicle Details:**<br>";
$vehicleInfo .= "🔹 Name: " . $vehicle_details['vehicle_name'] . "<br>";
$vehicleInfo .= "🔹 Year of Manufacture: " . $vehicle_details['vehicle_yom'] . "<br>";
$vehicleInfo .= "🔹 Engine No: " . $vehicle_details['vehicle_engine_no'] . "<br>";
if (isset($vehicle_details['vehicle_fuelType'])) {
$vehicleInfo .= "🔹 Fuel Type: " . $vehicle_details['vehicle_fuelType'] . "<br>";
$vehicleInfo .= "🔹 Seater: " . $vehicle_details['vehicle_seater'] . "<br>";
$vehicleInfo .= "🔹 Chassis Number: " . $vehicle_details['vehicle_chassisNo'] . "<br>";
}
$this->complied_information = $vehicleInfo;
// Send the message with vehicle details
$this->say($vehicleInfo);
$this->confirmVehicleDate($vehicleInfo);
// $this->bot->startConversation(new selectInsurerConversations());
}
protected function confirmVehicleDate($vehicle_info){
$question = Question::create("Confim your Details:")
->addButtons([
Button::create("✅ Confirm")->value("confirm"),
Button::create("🔁 Start Over")->value("restart"),
Button::create("❌ Cancel")->value('cancel')
]);
$this->bot->ask($question, function ($answer) use($vehicle_info) {
$path = $this->bot->userStorage()->get('path');
array_push($path,
$answer->getValue()
);
$this->bot->userStorage()->save([
'path' => $path
]);
switch ($answer->getValue()) {
case "confirm":
// $cityName = $response->getText();
$sent_staus = ChatbotHelper::sendQuotationRequestMail($vehicle_info);
if ($sent_staus){
$this->say('Thanks.Our Executive will contact you with the quotation soon.');
$this->bot->startConversation(new doYouWantToContinueConversation());
}else{
$this->say("There was a problem while requesting for a quotation please try again later.");
}
break;
case "restart":
$this->bot->startConversation(new vehicleFormConversation());
break;
case "cancel":
$this->say("Redirecting you to main menu...");
$this->bot->startConversation(new MainMenuConversation());
break;
default:
$this->say("Invalid selection. Please choose an option.");
$this->showHospitalMenu();
break;
}
});
}
}

View File

@ -0,0 +1,163 @@
<?php
namespace App\Controllers;
use App\Controllers\Chatbot\LoginToContiueConversation;
use BotMan\BotMan\BotMan;
use BotMan\BotMan\BotManFactory;
use BotMan\BotMan\Drivers\DriverManager;
use BotMan\BotMan\Cache\SymfonyCache;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use BotMan\Drivers\Web\WebDriver;
use App\Controllers\Chatbot\MainMenuConversation;
use App\Controllers\Chatbot\TypingMiddleware;
use App\Helpers\ChatbotHelper;
class ChatbotControllerNew extends BaseController
{
protected $myLogger;
protected $session;
protected $botman;
protected $isMemberLoggedIn;
public function __construct()
{
set_session_context('CHATBOT CON');
$this->myLogger = \Config\Services::mylogger();
$this->session = \Config\Services::session();
$sessionID = session_id();
$this->myLogger->logme('error', 'COOKIE - ' . $sessionID);
// $this->myLogger->logme('error', 'CALLED');
// Load BotMan driver
DriverManager::loadDriver(WebDriver::class);
// Setup BotMan with cache
$cache = new SymfonyCache(new FilesystemAdapter());
$this->botman = BotManFactory::create([], $cache);
// Create a new instance of BotMan
// $botman = resolve('botman');
// Attach the TypingMiddleware
// $this->botman->middleware->received(new TypingMiddleware());
// Retrieve user ID from session or request
$extras = request()->getGet('conf');
$extras = json_decode($extras);
$router = service('router');
$method = $router->methodName();
// $this->myLogger->logme('error', $method);
if (isset($extras) && $method == 'widget') {
$request = \Config\Services::request();
$cookieHeader = $request->getHeaderLine('Cookie');
$extras = $extras->parameters;
$this->session->set('CHATBOT_EMP_ID', $extras->employee_id);
$this->session->set('CHATBOT_ORIGIN', $extras->origin);
$this->session->set('CHATBOT_EMP_CODE', $extras->emp_code);
$this->session->set('CHATBOT_CLIENT_ID', $extras->client_id);
$this->session->set('CHATBOT_CLIENT_BRANCH_ID', $extras->client_branch_id);
if($this->session->get('CHATBOT_RANDOM_USER_ID') == '')
{
$this->session->set('CHATBOT_RANDOM_USER_ID', '-');
}
}
$log_message = [
'CHATBOT_EMP_ID' => $this->session->get('CHATBOT_EMP_ID'),
'CHATBOT_ORIGIN' => $this->session->get('CHATBOT_ORIGIN'),
'CHATBOT_EMP_CODE' => $this->session->get('CHATBOT_EMP_CODE'),
'CHATBOT_CLIENT_ID' => $this->session->get('CHATBOT_CLIENT_ID'),
'CHATBOT_CLIENT_BRANCH_ID' => $this->session->get('CHATBOT_CLIENT_BRANCH_ID'),
'CHATBOT_RANDOM_USER_ID' => $this->session->get('CHATBOT_RANDOM_USER_ID')
];
$log_message = implode(' | ', array_map(
fn($key, $value) => "{$key}: {$value}",
array_keys($log_message),
$log_message
));
$this->myLogger->logme('error', $log_message);
// print_rr(ChatbotHelper::getListOfPolicies());die();
}
public function index()
{
if($this->session->get('CHATBOT_RANDOM_USER_ID') == '-' || $this->session->get('CHATBOT_RANDOM_USER_ID') == '')
{
$user = $this->botman->getUser();
$id = $user->getId();
$this->myLogger->logme('error', ('TEST' . $id));
$this->session->set('CHATBOT_RANDOM_USER_ID', $id);
}
$this->isMemberLoggedIn = false;
$chat_session_info = get_chatbot_session_info();
if (!empty($chat_session_info['emp_id']) && !empty($chat_session_info['emp_code'] )){
$this->isMemberLoggedIn = true;
}
$this->botman->hears('.*', function ($bot) {
log_message("error","Inside Bot Type Function");
$bot->types(); // Typing indicator for the first message
// sleep(0.5); // Delay
});
// Start the Main Menu when user says "hi" or "start"
$this->botman->hears('start|hi|hello|help|help me', function (BotMan $bot) {
if ($this->isMemberLoggedIn){
// $bot->reply('Hi how can i assist?');
$bot->startConversation(new MainMenuConversation());
}else {
$bot->startConversation(new LoginToContiueConversation());
}
});
// $this->botman->hears('main_menu', function (BotMan $bot) {
// BotService::sendMainOptions($bot);
// });
// // Register Option Handlers
// $this->registerHandlers();
// Listen for Messages
$this->botman->listen();
}
// private function registerHandlers()
// {
// // Handling Policy and Claims
// $this->botman->hears('group:policy:{option}', [\App\Controllers\Chatbot\PolicyHandler::class, 'handle']);
// $this->botman->hears('group:claim:{option}', [\App\Libraries\Chatbot\ClaimHandler::class, 'handle']);
// }
public function chatbotest()
{
$this->loadLayout('chatbottest.php');
}
public function chatbot()
{
echo view('chatbot.php');
}
public function widget()
{
echo view('widget.php');
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,176 @@
<?php
namespace App\Controllers;
use App\Models\ClaimsCollectionV2DashboardModel;
use App\Models\ClaimDumpFileModel;
use CodeIgniter\API\ResponseTrait;
/**
* Claims Collection V2 KPI API (Metabase SQL as model methods).
*/
class ClaimsCollectionV2DashboardController extends BaseController
{
use ResponseTrait;
/**
* Resolve policy id from request; optional $fallback (e.g. route default for debug only).
*/
protected function resolvePolicyId(?int $fallback = null): int
{
$id = (int) (
$this->request->getGet('client_policy')
?? $this->request->getGet('client_policy_id')
?? $this->request->getPost('client_policy')
?? $this->request->getPost('client_policy_id')
?? 0
);
if ($id > 0) {
return $id;
}
if ($fallback !== null && $fallback > 0) {
return $fallback;
}
return 0;
}
/**
* Resolve KPI by Metabase numeric id or method slug.
*/
protected function resolveKpiMethod(string $kpiKey): ?string
{
$kpiKey = trim($kpiKey);
if ($kpiKey === '') {
return null;
}
if (in_array($kpiKey, ClaimsCollectionV2DashboardModel::KPI_MAP, true)) {
return $kpiKey;
}
if (ctype_digit($kpiKey)) {
$id = (int) $kpiKey;
return ClaimsCollectionV2DashboardModel::KPI_MAP[$id] ?? null;
}
return null;
}
/**
* JSON: single KPI by slug (e.g. incurred_ratio) or Metabase id (e.g. 207).
* Requires client_policy or client_policy_id.
*/
public function kpi(string $kpiMethod = '')
{
$policyId = $this->resolvePolicyId();
if ($policyId <= 0) {
return $this->respond([
'status' => false,
'message' => 'client_policy or client_policy_id is required.',
], 422);
}
$kpiMethod = $this->resolveKpiMethod($kpiMethod);
if ($kpiMethod === null) {
return $this->respond([
'status' => false,
'message' => 'Unknown KPI. Pass Metabase id or method slug.',
'allowed' => ClaimsCollectionV2DashboardModel::KPI_MAP,
], 404);
}
$model = new ClaimsCollectionV2DashboardModel();
$metabaseId = array_search($kpiMethod, ClaimsCollectionV2DashboardModel::KPI_MAP, true);
return $this->respond([
'status' => true,
'policy_id' => $policyId,
'kpi_id' => $metabaseId !== false ? (int) $metabaseId : null,
'kpi' => $kpiMethod,
'label' => ClaimsCollectionV2DashboardModel::KPI_LABELS[$kpiMethod] ?? $kpiMethod,
'rows' => $model->getKpi($kpiMethod, $policyId),
]);
}
/**
* JSON: all KPIs. Requires client_policy or client_policy_id.
*/
public function all()
{
$policyId = $this->resolvePolicyId();
if ($policyId <= 0) {
return $this->respond([
'status' => false,
'message' => 'client_policy or client_policy_id is required.',
], 422);
}
$generatedAt = (new ClaimDumpFileModel())->getGeneratedAtForPolicy($policyId);
// if ($generatedAt === null) {
// return $this->respond([
// 'status' => false,
// 'message' => 'claim_dump_date is mandatory. No claim dump date found for this policy.',
// ], 422);
// }
$model = new ClaimsCollectionV2DashboardModel();
return $this->respond([
'status' => true,
'policy_id' => $policyId,
'generated_at' => $generatedAt,
'data' => $model->getAllKpis($policyId),
]);
}
/**
* Admin preview UI (authMVC only) KPI grid for manual testing.
* Default policy id only in method signature; override via query or /preview/{id}.
*/
public function preview(int $policyId = 4687)
{
$policyId = $this->resolvePolicyId($policyId);
$path = $this->request->getUri()->getPath();
$isJwt = stripos($path, 'employeeRest') !== false;
$prefix = $isJwt ? 'employeeRest/claims-collection-v2' : 'util/claims-collection-v2';
return view('claims_collection_v2_dashboard', [
'policy_id' => $policyId,
'kpi_map' => ClaimsCollectionV2DashboardModel::KPI_MAP,
'kpi_labels' => ClaimsCollectionV2DashboardModel::KPI_LABELS,
'api_all_url' => base_url($prefix . '/all'),
'api_kpi_url' => base_url($prefix . '/kpi'),
]);
}
/**
* Admin check only: raw JSON on screen (no dashboard UI).
* Default policy id only here; override via ?client_policy= or /debug/{id}.
*/
public function debug(int $policyId = 4687)
{
$policyId = $this->resolvePolicyId($policyId);
if ($policyId <= 0) {
return $this->response
->setStatusCode(422)
->setBody('client_policy or client_policy_id is required.');
}
$model = new ClaimsCollectionV2DashboardModel();
$body = json_encode([
'status' => true,
'policy_id' => $policyId,
'data' => $model->getAllKpis($policyId),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
return $this->response
->setHeader('Content-Type', 'application/json; charset=UTF-8')
->setBody($body);
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace App\Controllers;
use CodeIgniter\API\ResponseTrait;
use App\Controllers\BaseController;
class ClaimsUploadController extends BaseController
{
use ResponseTrait;
protected $db;
public function __construct()
{
$this->db = \Config\Database::connect();
}
public function uploadDump()
{
$file = $this->request->getFile('file');
if (!$file || !$file->isValid()) {
return $this->respond([
'status' => 'failed',
'message' => 'Invalid file'
], 400);
}
if (!validate_upload_extension($file, UPLOAD_EXT_EXCEL)) {
return $this->respond([
'status' => 'failed',
'message' => 'Only Excel files (xls, xlsx, ods, csv) are allowed.'
], 400);
}
$uploadFile = storage_file_Upload($file, WRITEPATH . 'uploads/claims_dump/', UPLOAD_EXT_EXCEL);
if ($uploadFile === '') {
return $this->respond([
'status' => 'failed',
'message' => 'File upload failed'
], 500);
}
$data = [
'client_id' => $this->request->getPost('client_id'),
'tpa_id' => $this->request->getPost('tpa_id'),
'client_policy_id' => $this->request->getPost('client_policy_id'),
'from_date' => $this->request->getPost('from_date'),
'to_date' => $this->request->getPost('to_date'),
'upload_file' => $uploadFile,
// 'uploaded_by' => user_id()
];
$this->db->table('claims_dump_uploads')->insert($data);
$insertId = $this->db->insertID();
return $this->respond([
'status' => 'success',
'message' => 'Claims dump uploaded',
'id' => $insertId,
'upload_file' => $uploadFile,
]);
}
/**
* Download a claims_dump upload by claims_dump_uploads.id (local or S3).
*/
public function downloadDump($id = null)
{
$id = (int) ($id ?? $this->request->getGet('id'));
if ($id <= 0) {
$data['message'] = 'Invalid file id';
return view('errors/404', $data);
}
$record = $this->db->table('claims_dump_uploads')->where('id', $id)->get()->getRowArray();
if (empty($record) || empty($record['upload_file'])) {
$data['message'] = 'File record not found';
return view('errors/404', $data);
}
$fileName = basename((string) $record['upload_file']);
$storage = \Config\Services::getFileStorageService();
$result = $storage->download(WRITEPATH . 'uploads/claims_dump', $fileName);
if (! ($result['success'] ?? false)) {
$data['message'] = 'The Physical File Not Found';
return view('errors/404', $data);
}
$downloadAs = storage_upload_display_name($fileName);
if (! empty($result['path']) && is_file($result['path'])) {
return $this->response->download($result['path'], null)->setFileName($downloadAs);
}
if (! empty($result['content'])) {
return $this->response->download($downloadAs, $result['content'])->setFileName($downloadAs);
}
$data['message'] = 'The Physical File Not Found';
return view('errors/404', $data);
}
/**
* Resolve claims_dump file to a local path for any processors (local first, else S3).
*/
public function resolveDumpLocalPath(string $fileName): ?string
{
return storage_ensure_local_file(WRITEPATH . 'uploads/claims_dump', $fileName);
}
}

Some files were not shown because too many files have changed in this diff Show More