diff --git a/app/Controllers/ApiServiceController.php b/app/Controllers/ApiServiceController.php
index b69fe231..18eac66a 100644
--- a/app/Controllers/ApiServiceController.php
+++ b/app/Controllers/ApiServiceController.php
@@ -15,6 +15,7 @@ use App\Controllers\MediAssistApiController;
use App\Controllers\FhplApiController;
use App\Controllers\VoloApiController;
use App\Models\BatchFileModel;
+use App\Models\ClaimFilesModel;
use App\Models\FileModel;
use App\Helpers\TPADataCompareHelper;
use App\Helpers\TPADataCompareHelper2;
@@ -25,6 +26,7 @@ class ApiServiceController extends BaseController
// protected $format = 'json';
protected $db;
protected $employeePolicyModel;
+ protected $claimFilesModel;
protected $medi_assist_primary_key;
protected $vidal_primary_key;
protected $icici_primary_key;
@@ -36,6 +38,7 @@ class ApiServiceController extends BaseController
{
$this->db = \Config\Database::connect();
$this->employeePolicyModel = new EmployeePolicyModel();
+ $this->claimFilesModel = new ClaimFilesModel();
$this->medi_assist_primary_key = getenv('MEDI_ASSIST_PRIMARY_KEY_CONSTANT');
$this->vidal_primary_key = getenv('VIDAL_PRIMARY_KEY_CONSTANT');
$this->icici_primary_key = getenv('ICICI_PRIMARY_KEY_CONSTANT');
@@ -177,26 +180,20 @@ class ApiServiceController extends BaseController
$voloApiController = new VoloApiController();
$data['eCardDownload'] = $voloApiController->EcardRequest( $emp_code, $policy_no , $employee_policy[0]['tpa_id'] );
- }else{
-
- if($type == "download"){
- // direct download
- $data['eCardDownload'] = base_url('download-e-card/') . $employee_policy[0]['rand_string'].'/1';
- }else{
- if(isset($all_member) && !empty($all_member)){
- // view and download all members
- $data['eCardDownload'] = base_url('download-e-card/') . $employee_policy[0]['rand_string'].'/1/1';
- }else{
- // view and download single member
- $data['eCardDownload'] = base_url('download-e-card/') . $employee_policy[0]['rand_string'].'/0/1';
- }
- }
}
- $data['message'] = "E-card generated";
- if(empty($data['eCardDownload'])){
- $data['message'] = "E-card not generated";
- }
+ if (empty($data['eCardDownload'])) {
+ $data['eCardDownload'] = $this->buildDefaultEcardDownloadUrl(
+ $employee_policy[0]['rand_string'],
+ $type,
+ $all_member ?? null
+ );
+ }
+
+ $data['message'] = "E-card generated";
+ if(empty($data['eCardDownload'])){
+ $data['message'] = "E-card not generated";
+ }
} else {
$data['eCardDownload'] = null;
@@ -220,6 +217,19 @@ class ApiServiceController extends BaseController
}
}
+ private function buildDefaultEcardDownloadUrl(string $randString, string $type = 'download', $allMember = null): string
+ {
+ if ($type === 'download') {
+ return base_url('download-e-card/') . $randString . '/1';
+ }
+
+ if (!empty($allMember)) {
+ return base_url('download-e-card/') . $randString . '/1/1';
+ }
+
+ return base_url('download-e-card/') . $randString . '/0/1';
+ }
+
// Get TPAID
public function getTPAID()
{
@@ -583,6 +593,13 @@ class ApiServiceController extends BaseController
]);
}
+ if (! $this->claimFilesModel->hasPdfFileForTicket($claimId)) {
+ return $this->response->setJSON([
+ 'status' => false,
+ 'message' => 'No PDF file found for this claim'
+ ]);
+ }
+
$result = $this->pushClaims($claimId);
if ($result !== null && is_array($result)) {
diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php
index 5ec861eb..d14c8de7 100755
--- a/app/Controllers/ClientController.php
+++ b/app/Controllers/ClientController.php
@@ -763,7 +763,7 @@ class ClientController extends AdminController
public function saveDeposit()
{
- $rules = [
+ $rules = [
'amount' => [
'rules' => 'required|numeric|greater_than_equal_to[0]',
@@ -799,7 +799,7 @@ class ClientController extends AdminController
],
'cd_ac_no' => [
- 'rules' => 'required|regex_match[/^[a-zA-Z0-9\/\-_]+$/]',
+ 'rules' => 'required|regex_match[/^[a-zA-Z0-9\/\-_ ]+$/]',
'errors' => [
'required' => 'CD Account number is required.',
'regex_match' => 'CD Account number can only contain letters, numbers, hyphens(-), underscores(_), and slashes(/).',
@@ -874,12 +874,14 @@ class ClientController extends AdminController
$record_date = null;
}
+ $cd_master_data = $this->CDMasterModel->where('id', $cd_ac_pk)->where('is_active', 1)->first();
+
$data = [
'amount' => $sanitized_post_data['amount'] ?? null,
'sub_type_id' => $sanitized_post_data['sub_type_id'] ?? null,
'client_id' => $sanitized_post_data['client_id'] ?? null,
'client_policy_id' => null,
- 'cd_ac_no' => $cd_ac_no ?? null,
+ 'cd_ac_no' => $cd_master_data['cd_ac_no'] ?? null,
'cd_ac_pk' => $cd_ac_pk ?? null,
'endorsement_no' => null,
'insurer_id' => $sanitized_post_data['insurer_id'] ?? null,
diff --git a/app/Controllers/Docs/DocsController.php b/app/Controllers/Docs/DocsController.php
index ee117868..d85d77b0 100644
--- a/app/Controllers/Docs/DocsController.php
+++ b/app/Controllers/Docs/DocsController.php
@@ -63,6 +63,14 @@ class DocsController extends BaseController
['id' => 'tpa-recon', 'label' => 'TPA Recon', 'url' => 'docs/tpa-recon'],
['id' => 'eb-rack-rate-config', 'label' => 'EB rack rate config', 'url' => 'docs/eb-rack-rate-config'],
['id' => 'eb-rack-rate-calculation', 'label' => 'EB rack rate calculation', 'url' => 'docs/eb-rack-rate-calculation'],
+ ['id' => 'bds-insurer-statement', 'label' => 'BDS Insurer statement', 'url' => 'docs/bds-insurer-statement'],
+ ['id' => 'bds-commission', 'label' => 'BDS commission', 'url' => 'docs/bds-commission'],
+ ['id' => 'inception', 'label' => 'Inception', 'url' => 'docs/inception'],
+ ['id' => 'deletion', 'label' => 'Deletion', 'url' => 'docs/deletion'],
+ ['id' => 'correction', 'label' => 'Correction', 'url' => 'docs/correction'],
+ ['id' => 'si-enhancement', 'label' => 'SI Enhancement', 'url' => 'docs/si-enhancement'],
+ ['id' => 'non-eb-opportunities', 'label' => 'Non-EB Opportunities', 'url' => 'docs/non-eb-opportunities'],
+ ['id' => 'non-eb-claims', 'label' => 'Non-EB Claims', 'url' => 'docs/non-eb-claims'],
],
],
[
@@ -451,6 +459,226 @@ class DocsController extends BaseController
['label' => 'Related', 'href' => '#related'],
],
'prev' => ['label' => 'EB rack rate config', 'url' => 'docs/eb-rack-rate-config'],
+ 'next' => ['label' => 'BDS Insurer statement', 'url' => 'docs/bds-insurer-statement'],
+ ],
+
+ 'bds-insurer-statement' => [
+ 'view' => 'docs/bds-insurer-statement',
+ 'title' => 'BDS Insurer statement',
+ 'breadcrumb' => 'Features',
+ 'last_updated' => 'May 2026',
+ 'author' => 'Core Team',
+ 'read_time' => '12 min read',
+ 'toc' => [
+ ['label' => 'Overview', 'href' => '#overview'],
+ ['label' => 'Row validation logic', 'href' => '#row-validation-flowchart'],
+ ['label' => 'Key files and routes', 'href' => '#key-files-routes'],
+ ['label' => 'Statement list UI', 'href' => '#statement-list-ui'],
+ ['label' => 'uploadInsurerStatement','href' => '#upload-flow'],
+ ['label' => 'validateInsurerStatement', 'href' => '#validate-flow'],
+ ['label' => 'Validation steps', 'href' => '#validate-steps', 'level' => 'h3'],
+ ['label' => 'Validation error codes', 'href' => '#validation-errors', 'level' => 'h3'],
+ ['label' => 'updateInsurerStatement', 'href' => '#update-flow'],
+ ['label' => 'Excel column mapping', 'href' => '#excel-columns'],
+ ['label' => 'NHance source query', 'href' => '#nhance-source-query'],
+ ['label' => 'Invoice and delete', 'href' => '#invoice-and-delete'],
+ ['label' => 'Developer steps', 'href' => '#developer-steps'],
+ ['label' => 'Common pitfalls', 'href' => '#common-pitfalls'],
+ ['label' => 'Related BDS features', 'href' => '#related-bds'],
+ ],
+ 'prev' => ['label' => 'EB rack rate calculation', 'url' => 'docs/eb-rack-rate-calculation'],
+ 'next' => ['label' => 'BDS commission', 'url' => 'docs/bds-commission'],
+ ],
+
+ 'bds-commission' => [
+ 'view' => 'docs/bds-commission',
+ 'title' => 'BDS commission',
+ 'breadcrumb' => 'Features',
+ 'last_updated' => 'May 2026',
+ 'author' => 'Core Team',
+ 'read_time' => '10 min read',
+ 'toc' => [
+ ['label' => 'Overview', 'href' => '#overview'],
+ ['label' => 'Key files', 'href' => '#key-files'],
+ ['label' => 'Routes', 'href' => '#routes'],
+ ['label' => 'Commission file format', 'href' => '#commission-file-format'],
+ ['label' => 'Required columns', 'href' => '#required-columns', 'level' => 'h3'],
+ ['label' => 'Sample rows', 'href' => '#commission-file-samples', 'level' => 'h3'],
+ ['label' => 'Rule upload', 'href' => '#upload-flow'],
+ ['label' => 'Rules editor', 'href' => '#rules-editor'],
+ ['label' => 'Rule JSON shape', 'href' => '#rule-json'],
+ ['label' => 'Calculation API', 'href' => '#calculation-api'],
+ ['label' => 'Developer steps', 'href' => '#developer-steps'],
+ ['label' => 'Common pitfalls', 'href' => '#pitfalls'],
+ ],
+ 'prev' => ['label' => 'BDS Insurer statement', 'url' => 'docs/bds-insurer-statement'],
+ 'next' => ['label' => 'Inception', 'url' => 'docs/inception'],
+ ],
+
+ 'inception' => [
+ 'view' => 'docs/inception',
+ 'title' => 'Inception',
+ 'breadcrumb' => 'Features',
+ 'last_updated' => 'May 2026',
+ 'author' => 'Core Team',
+ 'read_time' => '8 min read',
+ 'toc' => [
+ ['label' => 'Overview', 'href' => '#overview'],
+ ['label' => 'Key files and routes', 'href' => '#key-files-routes'],
+ ['label' => 'Sync vs jobs', 'href' => '#sync-vs-jobs'],
+ ['label' => 'Format validation', 'href' => '#format-validation'],
+ ['label' => 'Format error codes', 'href' => '#format-errors', 'level' => 'h3'],
+ ['label' => 'reason JSON', 'href' => '#reason-json', 'level' => 'h3'],
+ ['label' => 'Data validation', 'href' => '#data-validation'],
+ ['label' => 'Data error codes', 'href' => '#data-errors', 'level' => 'h3'],
+ ['label' => 'Onboard preprocess', 'href' => '#preprocess'],
+ ['label' => 'Excel columns', 'href' => '#excel-columns'],
+ ['label' => 'Family row example', 'href' => '#family-example'],
+ ['label' => 'Developer steps', 'href' => '#developer-steps'],
+ ['label' => 'Common pitfalls', 'href' => '#pitfalls'],
+ ],
+ 'prev' => ['label' => 'BDS commission', 'url' => 'docs/bds-commission'],
+ 'next' => ['label' => 'Deletion', 'url' => 'docs/deletion'],
+ ],
+
+ 'deletion' => [
+ 'view' => 'docs/deletion',
+ 'title' => 'Deletion',
+ 'breadcrumb' => 'Features',
+ 'last_updated' => 'May 2026',
+ 'author' => 'Core Team',
+ 'read_time' => '7 min read',
+ 'toc' => [
+ ['label' => 'Overview', 'href' => '#overview'],
+ ['label' => 'Key files and routes', 'href' => '#key-files-routes'],
+ ['label' => 'Sync vs jobs', 'href' => '#sync-vs-jobs'],
+ ['label' => 'Format validation', 'href' => '#format-validation'],
+ ['label' => 'Format error codes', 'href' => '#format-errors', 'level' => 'h3'],
+ ['label' => 'Data validation', 'href' => '#data-validation'],
+ ['label' => 'employeeDisembark', 'href' => '#disembark'],
+ ['label' => 'TPA auto-deletion', 'href' => '#tpa-auto'],
+ ['label' => 'Excel columns', 'href' => '#excel-columns'],
+ ['label' => 'Row examples', 'href' => '#row-example'],
+ ['label' => 'Developer steps', 'href' => '#developer-steps'],
+ ['label' => 'Common pitfalls', 'href' => '#pitfalls'],
+ ],
+ 'prev' => ['label' => 'Inception', 'url' => 'docs/inception'],
+ 'next' => ['label' => 'Correction', 'url' => 'docs/correction'],
+ ],
+
+ 'correction' => [
+ 'view' => 'docs/correction',
+ 'title' => 'Correction',
+ 'breadcrumb' => 'Features',
+ 'last_updated' => 'May 2026',
+ 'author' => 'Core Team',
+ 'read_time' => '7 min read',
+ 'toc' => [
+ ['label' => 'Overview', 'href' => '#overview'],
+ ['label' => 'Key files and routes', 'href' => '#key-files-routes'],
+ ['label' => 'Sync vs jobs', 'href' => '#sync-vs-jobs'],
+ ['label' => 'Format validation', 'href' => '#format-validation'],
+ ['label' => 'Format error codes', 'href' => '#format-errors', 'level' => 'h3'],
+ ['label' => 'Data validation', 'href' => '#data-validation'],
+ ['label' => 'employeesCorrectionProcess', 'href' => '#correction-process'],
+ ['label' => 'Excel columns', 'href' => '#excel-columns'],
+ ['label' => 'Row examples', 'href' => '#row-example'],
+ ['label' => 'Developer steps', 'href' => '#developer-steps'],
+ ['label' => 'Common pitfalls', 'href' => '#pitfalls'],
+ ],
+ 'prev' => ['label' => 'Deletion', 'url' => 'docs/deletion'],
+ 'next' => ['label' => 'SI Enhancement', 'url' => 'docs/si-enhancement'],
+ ],
+
+ 'si-enhancement' => [
+ 'view' => 'docs/si-enhancement',
+ 'title' => 'SI Enhancement',
+ 'breadcrumb' => 'Features',
+ 'last_updated' => 'May 2026',
+ 'author' => 'Core Team',
+ 'read_time' => '7 min read',
+ 'toc' => [
+ ['label' => 'Overview', 'href' => '#overview'],
+ ['label' => 'Key files and routes', 'href' => '#key-files-routes'],
+ ['label' => 'Sync vs jobs', 'href' => '#sync-vs-jobs'],
+ ['label' => 'Format validation', 'href' => '#format-validation'],
+ ['label' => 'Format error codes', 'href' => '#format-errors', 'level' => 'h3'],
+ ['label' => 'Data validation', 'href' => '#data-validation'],
+ ['label' => 'employeesSIEnhanceProcess', 'href' => '#si-process'],
+ ['label' => 'Excel columns', 'href' => '#excel-columns'],
+ ['label' => 'Row examples', 'href' => '#row-example'],
+ ['label' => 'Developer steps', 'href' => '#developer-steps'],
+ ['label' => 'Common pitfalls', 'href' => '#pitfalls'],
+ ],
+ 'prev' => ['label' => 'Correction', 'url' => 'docs/correction'],
+ 'next' => ['label' => 'Non-EB Opportunities', 'url' => 'docs/non-eb-opportunities'],
+ ],
+
+ 'non-eb-opportunities' => [
+ 'view' => 'docs/non-eb-opportunities',
+ 'title' => 'Non-EB Opportunities',
+ 'breadcrumb' => 'Features',
+ 'last_updated' => 'May 2026',
+ 'author' => 'Core Team',
+ 'read_time' => '18 min read',
+ 'toc' => [
+ ['label' => 'End-to-end flow', 'href' => '#overview'],
+ ['label' => 'Key files', 'href' => '#key-files'],
+ ['label' => 'Google Sheet config', 'href' => '#google-sheet-config'],
+ ['label' => 'App ↔ Drive flow', 'href' => '#gsheet-app-drive-flow', 'level' => 'h3'],
+ ['label' => 'Before RFQ / QCR', 'href' => '#gsheet-prerequisites', 'level' => 'h3'],
+ ['label' => 'Sheet ID per product', 'href' => '#gsheet-product-config', 'level' => 'h3'],
+ ['label' => 'App-level config', 'href' => '#gsheet-app-config', 'level' => 'h3'],
+ ['label' => 'Create opportunity', 'href' => '#create-edit'],
+ ['label' => 'Edit flow', 'href' => '#edit-flow'],
+ ['label' => 'List table actions', 'href' => '#list-actions'],
+ ['label' => 'Action endpoint map', 'href' => '#action-map', 'level' => 'h3'],
+ ['label' => 'RFQ', 'href' => '#step-rfq'],
+ ['label' => 'QCR', 'href' => '#step-qcr'],
+ ['label' => 'Mail actions', 'href' => '#step-mails'],
+ ['label' => 'Placement', 'href' => '#step-placement'],
+ ['label' => 'Status lifecycle', 'href' => '#status-lifecycle'],
+ ['label' => 'Controller reference', 'href' => '#controller-reference'],
+ ['label' => 'Developer checklist', 'href' => '#developer-steps'],
+ ['label' => 'Common pitfalls', 'href' => '#pitfalls'],
+ ],
+ 'prev' => ['label' => 'SI Enhancement', 'url' => 'docs/si-enhancement'],
+ 'next' => ['label' => 'Non-EB Claims', 'url' => 'docs/non-eb-claims'],
+ ],
+
+ 'non-eb-claims' => [
+ 'view' => 'docs/non-eb-claims',
+ 'title' => 'Non-EB Claims',
+ 'breadcrumb' => 'Features',
+ 'last_updated' => 'May 2026',
+ 'author' => 'Core Team',
+ 'read_time' => '16 min read',
+ 'toc' => [
+ ['label' => 'End-to-end flow', 'href' => '#overview'],
+ ['label' => 'Why policy type 50', 'href' => '#policy-type-50-default'],
+ ['label' => 'Design assumption', 'href' => '#policy-type-50-assumption', 'level' => 'h3'],
+ ['label' => '50 vs selected policy', 'href' => '#policy-type-50-vs-selected-policy', 'level' => 'h3'],
+ ['label' => 'Where 50 is hardcoded', 'href' => '#policy-type-50-where-hardcoded', 'level' => 'h3'],
+ ['label' => 'Different status set', 'href' => '#policy-type-50-future', 'level' => 'h3'],
+ ['label' => 'Key files and routes', 'href' => '#key-files'],
+ ['label' => 'Route map', 'href' => '#route-map', 'level' => 'h3'],
+ ['label' => 'Access control', 'href' => '#access'],
+ ['label' => 'List and filter', 'href' => '#list-flow'],
+ ['label' => 'Create and edit', 'href' => '#create-edit'],
+ ['label' => 'Status-driven sections', 'href' => '#status-sections'],
+ ['label' => 'Auto-mail', 'href' => '#auto-mail'],
+ ['label' => 'Mail template CRUD', 'href' => '#mail-template-crud'],
+ ['label' => 'Template fields', 'href' => '#template-fields', 'level' => 'h3'],
+ ['label' => 'Template actions', 'href' => '#template-actions', 'level' => 'h3'],
+ ['label' => 'Notes', 'href' => '#notes'],
+ ['label' => 'Documents', 'href' => '#documents'],
+ ['label' => 'Reports', 'href' => '#reports'],
+ ['label' => 'Data model', 'href' => '#data-model'],
+ ['label' => 'Controller reference', 'href' => '#controller-reference'],
+ ['label' => 'Developer checklist', 'href' => '#developer-steps'],
+ ['label' => 'Common pitfalls', 'href' => '#pitfalls'],
+ ],
+ 'prev' => ['label' => 'Non-EB Opportunities', 'url' => 'docs/non-eb-opportunities'],
'next' => ['label' => 'Endpoints', 'url' => 'docs/endpoints'],
],
diff --git a/app/Controllers/EmployeeController.php b/app/Controllers/EmployeeController.php
index 05873250..3688f759 100755
--- a/app/Controllers/EmployeeController.php
+++ b/app/Controllers/EmployeeController.php
@@ -517,7 +517,7 @@ class EmployeeController extends AdminController
")
->join('client_policy', 'client_policy.id = batch_files.client_policy_id')
->join('insurers', 'client_policy.insurer_id = insurers.id')
- ->join('tpa', 'client_policy.tpa_id = tpa.id')
+ ->join('tpa', 'client_policy.tpa_id = tpa.id', 'left')
->join('client_branch', 'client_branch.id = batch_files.client_branch_id')
->join('policy_type', 'policy_type.id = client_policy.policy_type_id')
->join('clients', 'clients.id = client_policy.client_id')
@@ -574,7 +574,6 @@ class EmployeeController extends AdminController
->orderBy('batch_files.id', 'desc')
->find();
-
// dd($data['fileList']);die();
if ($this->request->getMethod() == "get") {
$this->loadLayout('import_export', $data);
@@ -4175,6 +4174,7 @@ class EmployeeController extends AdminController
}
// print_r($allUpdates);die();
// Do a single batch update for all families/members
+ log_message('error','[WellnessOnboard] DB update data - ' . json_encode($allUpdates));
if (!empty($allUpdates)) {
// 2nd param is the key to match on; here it's 'id'
$this->employeePolicyModel->updateBatch($allUpdates, 'id');
@@ -4255,25 +4255,36 @@ class EmployeeController extends AdminController
$emp_data_wo_tpa_id = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id);
// Reconcile DB records against TPA records and classify records for rec_type updates.
+ $consumedTpaIdsByEmpCode = [];
foreach ($emp_data_wo_tpa_id as $db_key => $db_row) {
- $tpa_temp_data = $tpaByEmpCode[$db_row['emp_code']] ?? [];
- $match = $this->reconcileDbWithTpa($db_row, $tpa_temp_data);
+ $empCode = (string) ($db_row['emp_code'] ?? '');
+ $empId = (string) ($db_row['id'] ?? '');
+ $empName = (string) ($db_row['name'] ?? '');
+ $tpa_temp_data = $tpaByEmpCode[$empCode] ?? [];
+ $excludeTpaIds = $consumedTpaIdsByEmpCode[$empCode] ?? [];
+ $match = $this->reconcileDbWithTpa($db_row, $tpa_temp_data, $excludeTpaIds);
$emp_data_wo_tpa_id[$db_key]['match'] = $match;
if (($match['status'] ?? '') === 'matched') {
$matchedTpaId = (int) ($match['tpa_record']['id'] ?? 0);
+ // echo 'matched' . $matchedTpaId . ' for emp_code ' . $empCode . PHP_EOL . '
+ BDS commission is a two-part flow: admins upload commission rules
+ (Excel) per insurer, month, and department; partner/BDS systems then call an API to
+ calculate payout for a policy using those rules.
+ In short: Admin (commission group): Runtime API:
+ Use the template from the upload screen (Download sample file) or copy from
+ Header text must match exactly (one row per rule, starting row 2). Empty cells are allowed and simply skip that condition. Template header + rows ( Example A — composite two-wheeler (from Example B — percentage four-wheeler: Example C — composite TP-only: Example D — flat amount: Tips: From
+ For successful uploads, action View Rules opens
+ Each rule is roughly: Calculation types in Rules with
+ BDS Insurer statement lets finance users upload an insurer-provided Excel statement,
+ validate each row against NHance policy transactions (
+ In short: Routes (prefix
+
+ Upload form fields ( Runs immediately after upload (and can be re-run manually in dev with a hard-coded Private helper trims Unicode spaces and strips zero-width / BOM characters from policy and endorsement values before comparison — avoids “looks equal” mismatches in Excel. Row keys in Runs only when validation returned Header row is removed; data columns used by validation/update: Download the canonical layout from the list page link →
+ Matching is on sanitized After a successful upload, users manage invoice lifecycle from the list (separate from upload/validate):
+
+ Correction updates existing member data on an active policy via Excel upload
+ (
+ Processing is in In short: Routes (group Rows grouped by EMP ID; for correction the critical check is On success → queues Per row:
+ Skips insert when a pending correction endorsement already exists for the same
+
+ Sets
+ TPA batch: If Fix DOB — one row, one field: Multiple fixes — use separate rows (same or different members):
+ Related:
+ Inception,
+ Deletion.
+
+ Deletion removes active members from a client policy via Excel upload
+ (
+ Core processing lives in In short: Routes (group Rows are grouped by EMP ID. For deletion, the main check is On success → queues Loads the Excel again and processes each non-empty row. Per matched member, four pending endorsement rows are inserted on
+ Returns an array of processed Delete one dependent — only that name appears; Self row is not required in the file. Result: endorsements for Arjun only (relationship ≠ Self). Delete entire family — list the Self row; disembark loads all active family members for that EMP ID. Result: pending endorsements for Self + all active dependents on that policy (same exit date/reason/claim from the row). Related: Inception (onboard pipeline uses the same upload screen and first two validation steps).
';
if ($matchedTpaId > 0) {
+ $consumedTpaIdsByEmpCode[$empCode][] = $matchedTpaId;
// If compare-fields list has differences, the row must be reviewed.
// Otherwise keep it as matched.
$recTypeById[$matchedTpaId] = empty($match['not_matching']) ? 'matched' : 'need_to_review';
}
} else {
- // No relation-level match found for this DB member.
- // Mark all candidate TPA rows for the same emp_code as review-required.
+ // echo 'not matched' . ' for emp_code ' . $empCode . PHP_EOL . ' - ' . $empId . ' - ' . $empName . ' - ' . '
';
+ // No match for this DB member: flag same-relation TPA rows not already paired.
+ $dbRel = strtolower(trim((string) ($db_row['relationship'] ?? '')));
foreach ($tpa_temp_data as $candidate) {
+ if (strtolower(trim((string) ($candidate['relation'] ?? ''))) !== $dbRel) {
+ continue;
+ }
$candidateId = (int) ($candidate['id'] ?? 0);
- if ($candidateId > 0) {
- $recTypeById[$candidateId] = 'need_to_review';
+ if ($candidateId > 0 && !in_array($candidateId, $excludeTpaIds, true)) {
+ $recTypeById[$candidateId] = '';
}
}
}
@@ -4306,76 +4317,75 @@ class EmployeeController extends AdminController
$db->transComplete();
}
- // this will match tpa api data with emp/emp policy table and update ref in tpa api data once
- $this->reconTpaApiDataWithEmployeepolicies(['file_id' => $file_id]);
+ // this will match tpa api data with emp/emp policy table and update ref in tpa api data once
+ $this->reconTpaApiDataWithEmployeepolicies(['file_id' => $file_id]);
} else {
- // echo 'else';die;
// Cached mode:
- // Read previously classified rows from rec_type, keep response shape compatible
- // with existing UI/export (`mismatch_data` still contains DB row + match payload).
+ // `not_in_nhance` stays sourced from persisted rec_type snapshot.
+ // `mismatch_data` must match compute-mode shape: every policy row with
+ // tpa_id NULL gets reconcileDbWithTpa against *all* active TPA rows for
+ // that emp_code (not only rows flagged need_to_review), otherwise second+
+ // loads drop rows / lose not_matching vs the first compute pass.
$not_in_nhance = $tpaApiDataModel->select('*')
->where('is_active', 1)
->where('file_id', $file_id)
->where('rec_type', 'not_in_nhance')
->findAll();
- $needToReviewRows = $tpaApiDataModel->select('*')
- ->where('is_active', 1)
+ $allActiveTpaRowsCached = $tpaApiDataModel->select('*')
->where('file_id', $file_id)
- ->where('rec_type', 'need_to_review')
+ ->where('is_active', 1)
->findAll();
- $needToReviewByEmpCode = [];
- foreach ($needToReviewRows as $row) {
- $needToReviewByEmpCode[$row['emp_code']][] = $row;
+ $tpaByEmpCodeCached = [];
+ foreach ($allActiveTpaRowsCached as $tpaRow) {
+ $tpaByEmpCodeCached[$tpaRow['emp_code']][] = $tpaRow;
}
- $baseRows = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id);
- foreach ($baseRows as $db_row) {
- $candidates = $needToReviewByEmpCode[$db_row['emp_code']] ?? [];
- if (empty($candidates)) {
- continue;
- }
-
- $selectedTpa = null;
- foreach ($candidates as $candidate) {
- // Prefer same-relation row to mimic reconcileDbWithTpa relation matching.
- if (strtolower((string) ($candidate['relation'] ?? '')) === strtolower((string) ($db_row['relationship'] ?? ''))) {
- $selectedTpa = $candidate;
- break;
+ $emp_data_wo_tpa_id = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id);
+ $consumedTpaIdsByEmpCodeCached = [];
+ foreach ($emp_data_wo_tpa_id as $db_key => $db_row) {
+ $empCode = (string) ($db_row['emp_code'] ?? '');
+ $tpa_temp_data = $tpaByEmpCodeCached[$empCode] ?? [];
+ $excludeTpaIds = $consumedTpaIdsByEmpCodeCached[$empCode] ?? [];
+ $match = $this->reconcileDbWithTpa($db_row, $tpa_temp_data, $excludeTpaIds);
+ $emp_data_wo_tpa_id[$db_key]['match'] = $match;
+ if (($match['status'] ?? '') === 'matched') {
+ $matchedTpaId = (int) ($match['tpa_record']['id'] ?? 0);
+ if ($matchedTpaId > 0) {
+ $consumedTpaIdsByEmpCodeCached[$empCode][] = $matchedTpaId;
}
}
- if ($selectedTpa === null) {
- $selectedTpa = $candidates[0];
- }
-
- $db_row['match'] = [
- 'status' => 'matched',
- 'tpa_record' => $selectedTpa,
- 'not_matching' => [],
- ];
- $emp_data_wo_tpa_id[] = $db_row;
}
}
+ // print_rr($emp_data_wo_tpa_id);die();
+
+
// Intentionally keep `not_in_tpa` live from current join/query logic
// (as requested) and do not source it from rec_type snapshot.
- $tpa_emp_codes = $tpaApiDataModel->select('emp_code')
+ $tpa_emp_codes = $tpaApiDataModel->select('ref')
->where('file_id', $file_id)
->where('is_active', 1)
- ->groupBy('emp_code')
+ ->groupBy('ref')
->findAll();
- $tpa_emp_codes = array_column($tpa_emp_codes, 'emp_code');
- $not_in_tpa = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id, $tpa_emp_codes);
+
+ // $tpa_emp_codes = array_column($tpa_emp_codes, 'ref');
+ $tpa_ref = array_column($tpa_emp_codes, 'ref');
+ $not_in_tpa = $this->employeePolicyModel->getTPADataVariationReport($client_id, $client_policy_id, $file_id, $tpa_ref);
+
+ // Format dates for UI/export (dd/mm/yyyy). Applied after reconciliation so
+ // reconcileDbWithTpa can still compare raw Y-m-d values from the database.
+ $this->formatVariationReportDataForDisplay($not_in_tpa, $not_in_nhance, $emp_data_wo_tpa_id);
if (!empty($not_in_tpa) || !empty($not_in_nhance) || !empty($emp_data_wo_tpa_id)) {
$not_in_nhance_button_enable_status = false;
- if(count($not_in_tpa))
- {
- foreach($not_in_nhance as $nih)
- {
- if($nih['ref'] === '' || empty($nih['ref']))
- {
+ if (count($not_in_nhance)) {
+ foreach ($not_in_nhance as $nih) {
+ $hasRefKey = array_key_exists('ref', $nih);
+ $ref = $hasRefKey ? $nih['ref'] : null;
+ $refIsEmpty = $ref === null || $ref === '';
+ if ($refIsEmpty) {
$not_in_nhance_button_enable_status = true;
break;
}
@@ -4410,6 +4420,21 @@ class EmployeeController extends AdminController
$not_in_nhance_deletion_count
);
+ $not_matched_count = 0;
+ $not_matched_data = [];
+ foreach ($emp_data_wo_tpa_id as $key => $row) {
+ $notMatching = $row['match']['not_matching'] ?? [];
+ if (isset($row['match']) && is_array($notMatching) && $notMatching !== []) {
+ $not_matched_count++;
+ $not_matched_data[] = $row;
+ }
+ }
+
+ $need_to_review_proceed_button_text = sprintf(
+ 'Proceed - Need to Review ( %d )',
+ $not_matched_count
+ );
+
$response = [
'not_in_tpa' => $not_in_tpa,
'not_in_nhance' => $not_in_nhance,
@@ -4418,6 +4443,9 @@ class EmployeeController extends AdminController
'not_in_nhance_inception_count' => $not_in_nhance_inception_count,
'not_in_nhance_deletion_count' => $not_in_nhance_deletion_count,
'not_in_nhance_proceed_button_text' => $not_in_nhance_proceed_button_text,
+ 'not_matched_count' => $not_matched_count,
+ 'not_matched_data' => $not_matched_data,
+ 'need_to_review_proceed_button_text' => $need_to_review_proceed_button_text,
];
if ($type === 'view') {
@@ -4889,10 +4917,12 @@ class EmployeeController extends AdminController
}
$mismatchRows = [];
+ $consumedTpaIdsByEmpCode = [];
foreach ($dbRows as $dbRow) {
+ $empCode = (string) ($dbRow['emp_code'] ?? '');
$tpaRows = $TpaApiDataModel->select('*')
- ->where('emp_code', $dbRow['emp_code'])
+ ->where('emp_code', $empCode)
->where('file_id', $batchFileId)
->where('is_active', 1)
->findAll();
@@ -4901,7 +4931,14 @@ class EmployeeController extends AdminController
continue;
}
- $match = $this->reconcileDbWithTpa($dbRow, $tpaRows);
+ $excludeTpaIds = $consumedTpaIdsByEmpCode[$empCode] ?? [];
+ $match = $this->reconcileDbWithTpa($dbRow, $tpaRows, $excludeTpaIds);
+ if (($match['status'] ?? '') === 'matched') {
+ $matchedTpaId = (int) ($match['tpa_record']['id'] ?? 0);
+ if ($matchedTpaId > 0) {
+ $consumedTpaIdsByEmpCode[$empCode][] = $matchedTpaId;
+ }
+ }
if (($match['status'] ?? '') !== 'matched') {
continue;
@@ -5190,9 +5227,12 @@ class EmployeeController extends AdminController
$rowsSkippedNoDiff = 0;
$rowsSkippedNoEmployee = 0;
+ $consumedTpaIdsByEmpCode = [];
+
foreach ($dbRows as $dbRow) {
+ $empCode = (string) ($dbRow['emp_code'] ?? '');
$tpaRows = $TpaApiDataModel->select('*')
- ->where('emp_code', $dbRow['emp_code'] ?? '')
+ ->where('emp_code', $empCode)
->where('file_id', $batchFileId)
->where('is_active', 1)
->findAll();
@@ -5201,7 +5241,8 @@ class EmployeeController extends AdminController
continue;
}
- $match = $this->reconcileDbWithTpa($dbRow, $tpaRows);
+ $excludeTpaIds = $consumedTpaIdsByEmpCode[$empCode] ?? [];
+ $match = $this->reconcileDbWithTpa($dbRow, $tpaRows, $excludeTpaIds);
if (($match['status'] ?? '') !== 'matched') {
continue;
@@ -5210,6 +5251,11 @@ class EmployeeController extends AdminController
$tpaRecord = $match['tpa_record'] ?? [];
$notMatching = $match['not_matching'] ?? [];
+ $matchedTpaId = (int) ($tpaRecord['id'] ?? 0);
+ if ($matchedTpaId > 0) {
+ $consumedTpaIdsByEmpCode[$empCode][] = $matchedTpaId;
+ }
+
if (!is_array($notMatching) || $notMatching === []) {
$rowsSkippedNoDiff++;
continue;
@@ -5524,57 +5570,169 @@ class EmployeeController extends AdminController
];
}
- public function reconcileDbWithTpa(array $db, array $tpaRows): array
+ /**
+ * Convert a single variation-report date value to dd/mm/yyyy for display.
+ */
+ private function formatVariationReportDateField($value): string
{
- // Name normalization
- $normalizeName = function ($name) {
+ if ($value === null || $value === '') {
+ return '';
+ }
+
+ $formatted = change_date_format((string) $value, null, 'd/m/Y');
+
+ return ($formatted !== null && $formatted !== '') ? (string) $formatted : (string) $value;
+ }
+
+ /**
+ * Format known date columns on one variation-report row (TPA or DB).
+ */
+ private function formatVariationReportRowDates(array $row, array $dateFields = ['dob', 'doj']): array
+ {
+ foreach ($dateFields as $field) {
+ if (!array_key_exists($field, $row)) {
+ continue;
+ }
+ if ($row[$field] === null || $row[$field] === '') {
+ continue;
+ }
+ $row[$field] = $this->formatVariationReportDateField($row[$field]);
+ }
+
+ return $row;
+ }
+
+ /**
+ * Apply dd/mm/yyyy formatting to all variation-report payloads returned to the UI/export.
+ */
+ private function formatVariationReportDataForDisplay(
+ array &$notInTpa,
+ array &$notInNhance,
+ array &$mismatchData
+ ): void {
+ foreach ($notInTpa as $idx => $row) {
+ $notInTpa[$idx] = $this->formatVariationReportRowDates($row);
+ }
+
+ foreach ($notInNhance as $idx => $row) {
+ $notInNhance[$idx] = $this->formatVariationReportRowDates($row);
+ }
+
+ foreach ($mismatchData as $idx => $row) {
+ $row = $this->formatVariationReportRowDates($row);
+ if (isset($row['match']['tpa_record']) && is_array($row['match']['tpa_record'])) {
+ $row['match']['tpa_record'] = $this->formatVariationReportRowDates($row['match']['tpa_record']);
+ }
+ $mismatchData[$idx] = $row;
+ }
+ }
+
+ /**
+ * Pair one Nhance policy row with a TPA API row for the same emp_code.
+ *
+ * When multiple dependents share a relation (e.g. two Sons), candidates are scored
+ * on name / DOB / gender and the best unique match wins. Already-paired TPA ids
+ * (same emp_code) can be passed via $excludeTpaIds so each TPA row maps once.
+ */
+ public function reconcileDbWithTpa(array $db, array $tpaRows, array $excludeTpaIds = []): array
+ {
+ $normalizeName = static function ($name) {
return strtolower(
- preg_replace('/[.\s_]+/', '', trim($name))
+ preg_replace('/[.\s_]+/', '', trim((string) $name))
);
};
- foreach ($tpaRows as $tpa) {
+ $normalizeRelation = static function ($relation) {
+ return strtolower(trim((string) $relation));
+ };
- // 1️⃣ emp_code + relation must match
- if (
- // ($db['emp_code'] ?? '') !== ($tpa['emp_code'] ?? '') ||
- strtolower($db['relationship']) !== strtolower($tpa['relation'])
- ) {
+ $dbRel = $normalizeRelation($db['relationship'] ?? '');
+
+ $candidates = [];
+ foreach ($tpaRows as $tpa) {
+ if (isset($tpa['match']['status']) && $tpa['match']['status'] === 'matched') {
continue;
}
- // 2️⃣ Field comparison
- $diff = [];
-
- if (
- ($db['name'] ?? '') !==
- ($tpa['name'] ?? '')
- ) {
- $diff[] = 'name';
+ $tpaId = (int) ($tpa['id'] ?? 0);
+ if ($tpaId > 0 && in_array($tpaId, $excludeTpaIds, true)) {
+ continue;
}
- if (($db['dob'] ?? '') !== ($tpa['dob'] ?? '')) {
- $diff[] = 'dob';
+ if ($normalizeRelation($tpa['relation'] ?? '') !== $dbRel) {
+ continue;
}
- if (
- strtoupper($db['gender'] ?? '') !==
- strtoupper($tpa['gender'] ?? '')
- ) {
- $diff[] = 'gender';
- }
-
- // 3️⃣ Match found
- return [
- 'status' => 'matched',
- 'tpa_record' => $tpa,
- 'not_matching' => $diff // empty = perfect match
- ];
+ $candidates[] = $tpa;
+ }
+
+ if ($candidates === []) {
+ return ['status' => 'no_match'];
+ }
+
+ $scoreCandidate = static function (array $tpa) use ($db, $normalizeName) {
+ $score = 0;
+ if ($normalizeName($db['name'] ?? '') === $normalizeName($tpa['name'] ?? '')) {
+ $score += 4;
+ }
+ if ((string) ($db['dob'] ?? '') === (string) ($tpa['dob'] ?? '')) {
+ $score += 2;
+ }
+ if (strtoupper(trim((string) ($db['gender'] ?? ''))) === strtoupper(trim((string) ($tpa['gender'] ?? '')))) {
+ $score += 1;
+ }
+
+ return $score;
+ };
+
+ $bestTpa = null;
+ $bestScore = -1;
+ foreach ($candidates as $tpa) {
+ $score = $scoreCandidate($tpa);
+ if ($score > $bestScore) {
+ $bestScore = $score;
+ $bestTpa = $tpa;
+ }
+ }
+
+ if ($bestTpa === null) {
+ return ['status' => 'no_match'];
+ }
+
+ // Multiple Son/Daughter rows: require a unique tie-breaker (name or DOB).
+ if (count($candidates) > 1) {
+ $topCount = 0;
+ foreach ($candidates as $tpa) {
+ if ($scoreCandidate($tpa) === $bestScore) {
+ $topCount++;
+ }
+ }
+ if ($topCount > 1 || $bestScore < 2) {
+ return ['status' => 'no_match'];
+ }
+ }
+
+ $diff = [];
+
+ if ($normalizeName($db['name'] ?? '') !== $normalizeName($bestTpa['name'] ?? '')) {
+ $diff[] = 'name';
+ }
+
+ if ((string) ($db['dob'] ?? '') !== (string) ($bestTpa['dob'] ?? '')) {
+ $diff[] = 'dob';
+ }
+
+ if (
+ strtoupper(trim((string) ($db['gender'] ?? ''))) !==
+ strtoupper(trim((string) ($bestTpa['gender'] ?? '')))
+ ) {
+ $diff[] = 'gender';
}
- // 4️⃣ No match found
return [
- 'status' => 'no_match'
+ 'status' => 'matched',
+ 'tpa_record' => $bestTpa,
+ 'not_matching' => $diff,
];
}
diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php
index 9518d202..3066ada1 100755
--- a/app/Controllers/EmployeeRestController.php
+++ b/app/Controllers/EmployeeRestController.php
@@ -3785,14 +3785,6 @@ class EmployeeRestController extends AdminController
$this->handleCliamFiles($file_data, $ticket_id, $ticket_message_id);
- // Merge all uploaded PDFs for this ticket into a single combined PDF
- try {
- helper('merge_pdf');
- merge_ticket_pdfs((int) $ticket_id);
- } catch (\Throwable $e) {
- log_message('error', 'EmployeeRestController::initiateClaim | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage());
- }
-
$mail_sent_status = ($this->ticketController->sendAutoMailTrigger($ticket_id));
if (gettype($mail_sent_status) == 'array') {
$message = 'Claim Iniated Successfully';
@@ -3931,6 +3923,15 @@ class EmployeeRestController extends AdminController
}
}
+ if (! empty($insert_ids)) {
+ try {
+ helper('merge_pdf');
+ merge_ticket_pdfs((int) $ticket_id);
+ } catch (\Throwable $e) {
+ log_message('error', 'EmployeeRestController::handleCliamFiles | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage());
+ }
+ }
+
$count = $this->ticketMaster->where('id', $ticket_id)->where('is_active', 1)->where('tpa_claim_push_reference_no IS NULL')->countAllResults();
log_message('error', 'Ticket validation | Ticket ID: ' . $ticket_id . ' | Matching active tickets with NULL TPA reference: ' . $count);
if ($count > 0 && $pdf_exist_in_the_file && $tpa_claim_push == true) {
@@ -5452,14 +5453,6 @@ class EmployeeRestController extends AdminController
$result = $this->handleCliamFiles($file_data, $ticket_id, null, false, true);
if (! empty($result)) {
- // Merge all uploaded PDFs for this ticket into a single combined PDF
- try {
- helper('merge_pdf');
- merge_ticket_pdfs((int) $ticket_id);
- } catch (\Throwable $e) {
- log_message('error', 'EmployeeRestController::uploadIRDocs | merge_ticket_pdfs failed | ticket_id=' . $ticket_id . ' | ' . $e->getMessage());
- }
-
// $this->ticketMaster->where('id', $ticket_id)->set(['required_docs', $required_docs])->update();
db_connect()->query(
"UPDATE ticket_master SET required_docs = ? WHERE id = ?",
diff --git a/app/Controllers/FhplApiController.php b/app/Controllers/FhplApiController.php
index cbaf1115..6d684e1c 100644
--- a/app/Controllers/FhplApiController.php
+++ b/app/Controllers/FhplApiController.php
@@ -554,30 +554,32 @@ class FhplApiController extends BaseController
foreach ($employeePolicyData as $policy) {
$hasMatchForThisPolicy = false;
foreach ($allMembers as $m) {
+ $apiRelation = map_relationship(trim($m['RELATION'] ?? ''));
if (
- strtolower(trim($policy['name'])) === strtolower(trim($m['BENEFICIARY_NAME'] ?? '')) &&
- ($policy['emp_code'] ?? '') == ($m['EMPLOYEE_ID'] ?? '') &&
- strtolower($policy['relationship']) === strtolower($m['RELATION'] ?? '')
+ strtolower(trim((string) ($policy['name'] ?? ''))) === strtolower(trim((string) ($m['BENEFICIARY_NAME'] ?? '')))
+ && trim((string) ($policy['emp_code'] ?? '')) === trim((string) ($m['EMPLOYEE_ID'] ?? ''))
+ && strtolower(trim((string) ($policy['relationship'] ?? ''))) === strtolower(trim((string) $apiRelation))
) {
-
$hasMatchForThisPolicy = true;
-
- $sql = "UPDATE employee_polices SET tpa_id = ? WHERE id = ?";
- $this->db->query($sql, [$m['MEMBERSHIP_NO'], $policy['emp_policy_id']]);
+ $tpaId = $m['MEMBERSHIP_NO'] ?? $m['MEMBERSHIP_NO'] ?? null;
+ $sql = 'UPDATE employee_polices SET tpa_id = ? WHERE id = ?';
+ $this->db->query($sql, [$tpaId, $policy['emp_policy_id']]);
// for e-card send
- if(strtolower(trim($policy['relationship'])) == 'self'){
+ if (strtolower(trim((string) ($policy['relationship'] ?? ''))) === 'self') {
$employee_policy_ids[] = $policy['emp_policy_id'];
}
-
+
if ($this->db->affectedRows() > 0) {
- $updated++;
- log_message('error', "FHPL - TPA ID Pull Updated tpa_id={$m['MEMBERSHIP_NO']} for emp_policy_id={$policy['emp_policy_id']} policy={$policyNo}");
+ $updated++;
+ log_message('error', "FHPL - TPA ID Pull Updated tpa_id={$tpaId} for emp_policy_id={$policy['emp_policy_id']} policy={$policyNo}");
} else {
- log_message('error', "FHPL - TPA ID Pull No update (already set or not matched) for emp_policy_id={$policy['emp_policy_id']} policy={$policyNo}");
+ log_message('error', "FHPL - TPA ID Pull No update (already set or not matched) for emp_policy_id={$policy['emp_policy_id']} policy={$policyNo}");
}
+
+ break;
}
}
@@ -1070,7 +1072,7 @@ class FhplApiController extends BaseController
'gender' => format_gender_v2($row['GENDER'] ?? null),
'self' => strtolower($row['RELATION'] ?? '') === 'self' ? 1 : 0,
- 'tpa_id' => trim($row['TPA_TPADETAIL_ID'] ?? null),
+ 'tpa_id' => trim($row['MEMBERSHIP_NO'] ?? null),
'age' => is_numeric($row['AGE'] ?? null) ? (int) $row['AGE'] : null,
'is_active' => 1,
diff --git a/app/Controllers/LeadsController.php b/app/Controllers/LeadsController.php
index 7f347d6f..0b1fc090 100644
--- a/app/Controllers/LeadsController.php
+++ b/app/Controllers/LeadsController.php
@@ -368,8 +368,10 @@ class LeadsController extends BaseController
$id = $this->request->getPost('id');
$actual_lead_id = $this->request->getPost('actual_lead_id');
$actual_lead_id = ! empty($actual_lead_id) ? $actual_lead_id : null;
- $postData = $this->request->getPost();
- $data = $this->prepareLeadData();
+
+ $this->normalizeClaimHistoryPostData();
+ $postData = $this->request->getPost();
+ $data = $this->prepareLeadData();
$rules = [
@@ -526,47 +528,65 @@ class LeadsController extends BaseController
];
if ((int) $this->request->getPost('claim_history') === 1) {
- $rules['first_year.*'] = [
- 'rules' => 'required|regex_match[/^\d{4}-\d{4}$/]',
- 'errors' => ['required' => 'Claim Year is required for all entries.', 'regex_match' => 'Year must be in format YYYY-YYYY.'],
- ];
- $rules['first_policy_type_.*'] = [
- 'rules' => 'required|regex_match[/^[a-zA-Z0-9 _-]+$/]',
- 'errors' => ['required' => 'Policy Type is required in Claim History.',
- 'regex_match' => 'Policy Type only letters, numbers, space, hyphens and underscores are allowed',
- ],
- ];
- $rules['first_date_of_loss_.*'] = [
- 'rules' => 'required|regex_match[/^[0-9]{2}-[0-9]{2}-[0-9]{4}$/]',
- 'errors' => ['required' => 'Date of Loss is required.',
- 'regex_match' => 'Date of Loss must be inValid format.'],
- ];
- $rules['first_cause_of_loss.*'] = [
- 'rules' => 'required|regex_match[/^[a-zA-Z0-9 _-]+$/]',
- 'errors' => [
- 'required' => 'Cause of Loss is required.',
- 'regex_match' => 'Cause of Loss only letters, numbers, space, hyphens and underscores are allowed',
- ],
- ];
- $rules['first_claim_amount.*'] = [
- 'rules' => 'required|numeric',
- 'errors' => [
- 'required' => 'Claim Amount is required.',
- 'numeric' => 'Claim Amount must be a number.',
- ],
- ];
- $rules['first_settled_amount.*'] = [
- 'rules' => 'required|numeric',
- 'errors' => ['required' => 'Settled Amount is required.',
- 'numeric' => 'Settled Amount must be a number.'],
- ];
- $rules['first_claim_status.*'] = [
- 'rules' => 'required|alpha_space',
- 'errors' => [
- 'required' => 'Claim Status is required.',
- 'alpha_space' => 'Claim Status should only contain letters and spaces.',
- ],
- ];
+ $nilClaimsRows = $this->resolveNilClaimRowFlags($postData);
+ $claimRowCount = $this->getClaimHistoryRowCount($postData, $nilClaimsRows);
+
+ for ($claimIndex = 0; $claimIndex < $claimRowCount; $claimIndex++) {
+ $rules["first_year.$claimIndex"] = [
+ 'rules' => 'required|regex_match[/^\d{4}-\d{4}$/]',
+ 'errors' => [
+ 'required' => 'Claim Year is required for all entries.',
+ 'regex_match' => 'Year must be in format YYYY-YYYY.',
+ ],
+ ];
+
+ if (! empty($nilClaimsRows[$claimIndex])) {
+ continue;
+ }
+
+ $rules["first_policy_type_.$claimIndex"] = [
+ 'rules' => 'required|regex_match[/^[a-zA-Z0-9 _-]+$/]',
+ 'errors' => [
+ 'required' => 'Policy Type is required in Claim History.',
+ 'regex_match' => 'Policy Type only letters, numbers, space, hyphens and underscores are allowed',
+ ],
+ ];
+ $rules["first_date_of_loss_.$claimIndex"] = [
+ 'rules' => 'required|regex_match[/^[0-9]{2}-[0-9]{2}-[0-9]{4}$/]',
+ 'errors' => [
+ 'required' => 'Date of Loss is required.',
+ 'regex_match' => 'Date of Loss must be inValid format.',
+ ],
+ ];
+ $rules["first_cause_of_loss.$claimIndex"] = [
+ 'rules' => 'required|regex_match[/^[a-zA-Z0-9 _-]+$/]',
+ 'errors' => [
+ 'required' => 'Cause of Loss is required.',
+ 'regex_match' => 'Cause of Loss only letters, numbers, space, hyphens and underscores are allowed',
+ ],
+ ];
+ $rules["first_claim_amount.$claimIndex"] = [
+ 'rules' => 'required|numeric',
+ 'errors' => [
+ 'required' => 'Claim Amount is required.',
+ 'numeric' => 'Claim Amount must be a number.',
+ ],
+ ];
+ $rules["first_settled_amount.$claimIndex"] = [
+ 'rules' => 'required|numeric',
+ 'errors' => [
+ 'required' => 'Settled Amount is required.',
+ 'numeric' => 'Settled Amount must be a number.',
+ ],
+ ];
+ $rules["first_claim_status.$claimIndex"] = [
+ 'rules' => 'required|alpha_space',
+ 'errors' => [
+ 'required' => 'Claim Status is required.',
+ 'alpha_space' => 'Claim Status should only contain letters and spaces.',
+ ],
+ ];
+ }
}
}
@@ -576,65 +596,75 @@ class LeadsController extends BaseController
$claimHistoryPolicyTypes = [1, 6, 7];
$hasClaimHistoryPolicy = count(array_intersect($postedPolicyTypeIds, $claimHistoryPolicyTypes)) > 0;
$isEbClaimHistory = $leadFormType === 1
- && in_array((int) ($postData['lead_type'] ?? 0), [2, 3], true)
+ && in_array((int) ($postData['lead_type'] ?? 0), [1, 2, 3], true)
&& $hasClaimHistoryPolicy
&& (int) ($postData['claim_history'] ?? 0) === 1;
if ($isEbClaimHistory) {
- $rules['first_year.*'] = [
- 'rules' => 'required|regex_match[/^\d{4}-\d{4}$/]',
- 'errors' => [
- 'required' => 'Claim Year is required for all entries.',
- 'regex_match' => 'Year must be in format YYYY-YYYY.',
- ],
- ];
- $rules['emp_id.*'] = [
- 'rules' => 'required',
- 'errors' => ['required' => 'Employee ID is required in Claim History.'],
- ];
- $rules['emp_name.*'] = [
- 'rules' => 'required',
- 'errors' => ['required' => 'Employee Name is required in Claim History.'],
- ];
- $rules['gender.*'] = [
- 'rules' => 'required|in_list[Female,Male]',
- 'errors' => [
- 'required' => 'Gender is required in Claim History.',
- 'in_list' => 'Gender must be Female or Male.',
- ],
- ];
- $rules['designation.*'] = [
- 'rules' => 'required',
- 'errors' => ['required' => 'Designation is required in Claim History.'],
- ];
- $rules['sum_insured.*'] = [
- 'rules' => 'required|numeric',
- 'errors' => [
- 'required' => 'Sum Insured is required in Claim History.',
- 'numeric' => 'Sum Insured must be a number.',
- ],
- ];
- $rules['first_death_date.*'] = [
- 'rules' => 'required|regex_match[/^[0-9]{2}-([0-9]{2}|[A-Za-z]{3})-[0-9]{4}$/]',
- 'errors' => [
- 'required' => 'Date of Death is required.',
- 'regex_match' => 'Date of Death must be in DD-MM-YYYY or DD-MMM-YYYY format.',
- ],
- ];
- $rules['first_cause_of_death.*'] = [
- 'rules' => 'required|in_list[natural_death,suicide,accident,cardiac_arrest,septic_shock,heart_attack]',
- 'errors' => [
- 'required' => 'Nature/Cause Of Death is required.',
- 'in_list' => 'Nature/Cause Of Death is invalid.',
- ],
- ];
- $rules['first_claim_amount.*'] = [
- 'rules' => 'required|numeric',
- 'errors' => [
- 'required' => 'Claim/Settled Amount is required.',
- 'numeric' => 'Claim/Settled Amount must be a number.',
- ],
- ];
+ $nilClaimsRows = $this->resolveNilClaimRowFlags($postData);
+ $claimRowCount = $this->getClaimHistoryRowCount($postData, $nilClaimsRows);
+
+ for ($claimIndex = 0; $claimIndex < $claimRowCount; $claimIndex++) {
+ $rules["first_year.$claimIndex"] = [
+ 'rules' => 'required|regex_match[/^\d{4}-\d{4}$/]',
+ 'errors' => [
+ 'required' => 'Claim Year is required for all entries.',
+ 'regex_match' => 'Year must be in format YYYY-YYYY.',
+ ],
+ ];
+
+ if (! empty($nilClaimsRows[$claimIndex])) {
+ continue;
+ }
+
+ $rules["emp_id.$claimIndex"] = [
+ 'rules' => 'required',
+ 'errors' => ['required' => 'Employee ID is required in Claim History.'],
+ ];
+ $rules["emp_name.$claimIndex"] = [
+ 'rules' => 'required',
+ 'errors' => ['required' => 'Employee Name is required in Claim History.'],
+ ];
+ $rules["gender.$claimIndex"] = [
+ 'rules' => 'required|in_list[Female,Male]',
+ 'errors' => [
+ 'required' => 'Gender is required in Claim History.',
+ 'in_list' => 'Gender must be Female or Male.',
+ ],
+ ];
+ $rules["designation.$claimIndex"] = [
+ 'rules' => 'required',
+ 'errors' => ['required' => 'Designation is required in Claim History.'],
+ ];
+ $rules["sum_insured.$claimIndex"] = [
+ 'rules' => 'required|numeric',
+ 'errors' => [
+ 'required' => 'Sum Insured is required in Claim History.',
+ 'numeric' => 'Sum Insured must be a number.',
+ ],
+ ];
+ $rules["first_death_date.$claimIndex"] = [
+ 'rules' => 'required|regex_match[/^[0-9]{2}-([0-9]{2}|[A-Za-z]{3})-[0-9]{4}$/]',
+ 'errors' => [
+ 'required' => 'Date of Death is required.',
+ 'regex_match' => 'Date of Death must be in DD-MM-YYYY or DD-MMM-YYYY format.',
+ ],
+ ];
+ $rules["first_cause_of_death.$claimIndex"] = [
+ 'rules' => 'required|in_list[natural_death,suicide,accident,cardiac_arrest,septic_shock,heart_attack]',
+ 'errors' => [
+ 'required' => 'Nature/Cause Of Death is required.',
+ 'in_list' => 'Nature/Cause Of Death is invalid.',
+ ],
+ ];
+ $rules["first_claim_amount.$claimIndex"] = [
+ 'rules' => 'required|numeric',
+ 'errors' => [
+ 'required' => 'Claim/Settled Amount is required.',
+ 'numeric' => 'Claim/Settled Amount must be a number.',
+ ],
+ ];
+ }
}
// 1. MANUALLY VALIDATE FILES BEFORE PROCESSING
@@ -2866,7 +2896,9 @@ class LeadsController extends BaseController
//claim history new sheet;
if ($claim_history == 1 && !empty($rfq_data['fin_years_claims'])) {
- $claim_details = json_decode($rfq_data['fin_years_claims'], true) ?? [];
+ $claim_details = $this->sanitizeClaimDetailsForExport(
+ json_decode($rfq_data['fin_years_claims'], true) ?? []
+ );
if (! empty($claim_details['finyear'])) {
@@ -4654,6 +4686,209 @@ class LeadsController extends BaseController
}
}
+ private function normalizeClaimHistoryPostData(): void
+ {
+ $post = $this->request->getPost();
+
+ if ((int) ($post['claim_history'] ?? 0) !== 1) {
+ return;
+ }
+
+ $rows = $this->getFinyearRowsFromPost($post);
+
+ if ($rows === []) {
+ return;
+ }
+
+ $leadFormType = (int) ($post['lead_form_type'] ?? 1);
+ $claimArrays = $leadFormType === 2
+ ? $this->buildNonEbClaimHistoryPostArrays($rows)
+ : $this->buildEbClaimHistoryPostArrays($rows);
+
+ $this->request->setGlobal('post', array_merge($post, $claimArrays));
+ }
+
+ /**
+ * Remove internal nil_claims flag from stored/exported claim rows.
+ *
+ * @param arrayOverview
+
+
+
+
+RuleImportController + commission_file_upload.php — import and manage rules.POST getCommission → InsuranceCommissionController::initiateCommissionCalc — read JSON, pick first matching rule, return payout.writable/uploads/commission/rules/{MONYYYY}/{insurer_id}_{department}.json (e.g. SEP2025/5_motor.json).Key files
+
+
+
+
+
+
+
+
+ Part File
+ Upload list UI app/Views/commission_file_upload.php
+ Rules editor UI app/Views/commission_rules_list.php
+ Upload and editor API app/Controllers/RuleImportController.php
+ Excel parsing ruleImportService (via Config\Services::ruleImportService())
+ Payout calculation API app/Controllers/InsuranceCommissionController.php
+
+Upload metadata DB commission_files (CommissionFilesModel)Routes
+
+
+
+
+
+
+
+
+ Route Handler
+ GET/POST commission/listcommissionFileUploadList
+ POST commission/uploadupload
+ GET commission/sample_fileSample CSV download
+ GET commission/downloadErrorFileAnnotated error Excel
+ GET commission/checkSameEntryDuplicate insurer + month + department check
+ GET commission/rules/list/(:id)ruleList — rules editor page
+ POST commission/rules/save/saveRule
+ POST commission/rules/remove/removeRule
+ GET commission/checkRuleUsageWhether rule is used on partner policies
+
+GET commission/deleteCommissionData/(:id)Soft-delete file + mark rules deleted
+
+POST getCommission
+ → InsuranceCommissionController::initiateCommissionCalc
+ → filter: CommissionApiFilterHow to build the commission file
+
+public/sample_excel/sample_commission.csv. Dev copies also live under
+ writable/uploads/commission/files/ (e.g. sample_commission.csv,
+ Sample_commission_file-New.xlsx). After a successful import, see the generated JSON under
+ writable/uploads/commission/rules/{MONYYYY}/ (example: NOV2025/1_motor.json).
+writable/uploads/commission/files/ use legacy headers
+ (Rule Name, Commission Params(TP:OD:PA)). The importer expects the
+ S.No layout below (RuleImportService). Wrong headers fail with
+ “Missing required columns”.
+ Required columns (row 1 headers)
+
+
+
+
+
+
+
+
+ Column Purpose Example
+ S.No Serial (not used in logic) 1
+ Premium Type Maps to policy_typeOD, TP, COM
+ Vehicle Type vehicle_type; comma = multiple (IN)Two Wheeler, Four Wheeler
+ Vehicle Sub Type vehicle_sub_typeCar, PCV
+ Make / Model Vehicle make and model Honda, i10
+ CC Min / CC Max Cubic capacity range 1000, 3000
+ Fuel Type Comma-separated fuels Petrol, Diesel
+ Vehicle Age Min / Max Vehicle age range (years) 1, 5
+ Vehicle Weight Min / Max Weight range (kg) 2000, 3000
+ RTO State / RTO Code geo_rto_state, geo_rto_cityRJ, 41
+ Renewal Type renewal_typeOnline, Cash, Card
+ Commission Type percentage, composite, flat, tieredpercentage
+ Commission Value % or flat amount (required for percentage / flat) 15 or 500
+ Commission Params (TP) Composite: % on TP premium 18
+ Commission Params (OD) Composite: % on OD premium 10
+
+Commission Params (PA) Composite: % on PA premium 0 or empty Commission types (what to fill)
+
+
+
+
+
+
+
+
+ Type Fill in sheet Becomes in JSON
+
+
+ percentageCommission Value = e.g.
+ 1010% of
+ premium
+
+
+ compositeLeave Value empty; set TP / OD / PA param columns (percent each)
+ Split % on
+ tp_premium, od_premium, pa_premium
+
+
+
+ flatCommission Value = fixed rupee amount e.g.
+ 500Fixed payout on
+ premiumSample rows (from project files)
+
+public/sample_excel/sample_commission.csv):
+
+S.No,Premium Type,Vehicle Type,...,Commission Type,Commission Value,Commission Params (TP),Commission Params (OD),Commission Params (PA)
+1,OD,Car,...,percentage,15,,,
+2,TP,Two Wheeler,...,composite,,18,10,
+3,COM,PCV,...,percentage,25,,,
+4,COM,GCV,...,composite,,,10,writable/.../rules/NOV2025/1_motor.json):
+
+
+Two Wheeler, CC Min/Max = 100composite, TP = 10, OD = 25
+
+
+Four Wheeler, CC = 1000, Vehicle Age Min/Max = 5percentage, Commission Value = 10
+
+
+TP, Vehicle Type = Four Wheelercomposite, Commission Params (TP) = 10
+
+
+Two Wheeler,Four Wheeler (comma → matches either)flat, Commission Value = 500
+
+
+.csv, .xlsx, .xls, .ods.motor, health; import logic is built for motor columns today.Rule upload
+
+commission_file_upload.php the user picks insurer, commission month, department (motor / health), and an Excel/CSV file.
+
+
+checkSameEntry — if a successful upload already exists for the same trio, SweetAlert offers Overwrite or Append (overwrite=1 or 0 on POST).upload — stores file under writable/uploads/commission/files/, inserts commission_files row (pending).ruleImportService->processUpload() — validates Excel rows; on success returns rules array; on failure returns annotated_file for download.rules/{MONYYYY}/{insurer_id}_{department}.json; sets file_status=success and rules_count.file_status=failed; user downloads annotated_{filename} via downloadErrorFile.Rules editor
+
+commission/rules/list/{file_id} (commission_rules_list.php).
+
+
+
+saveRule — create or update a rule (conditions + calculation + name) in the JSON via updateCommissionRules().removeRule — soft-delete one rule (is_deleted=true).checkRuleUsage — warns if partner_policy.commission_applied_rule references the rule.file_id as deleted in JSON, then sets commission_files.is_active=0.Rule JSON shape
+
+
+
+{
+ "id": "rule_…",
+ "name": "Rule name",
+ "department": "motor",
+ "file_id": 12,
+ "is_deleted": false,
+ "conditions": [
+ { "field": "vehicle_type", "operator": "==", "value": "car" }
+ ],
+ "calculation": {
+ "type": "percentage",
+ "value": 10,
+ "on": "premium"
+ }
+}InsuranceCommissionController: percentage, composite, fixed. Conditions support ==, !=, >, >=, <, <=, between, in.Commission calculation API
+
+initiateCommissionCalc() expects POST/JSON including at least:
+
+
+policy_issue_date — used to pick folder {MON}{YEAR} (e.g. SEP2025)insurer_iddepartment — motor, health, etc.premium, od_premium)is_deleted: false are loaded; the first matching rule wins (no priority field yet).Developer steps
+
+
+
+
+/commission/list (logged-in admin).POST getCommission with the same insurer, department, and a policy_issue_date in that commission month.Common pitfalls
+
+
+
diff --git a/app/Views/docs/bds-insurer-statement.php b/app/Views/docs/bds-insurer-statement.php
new file mode 100644
index 00000000..a504be59
--- /dev/null
+++ b/app/Views/docs/bds-insurer-statement.php
@@ -0,0 +1,309 @@
+
+
+policy_issue_date to resolve the same MONYYYY folder.status and code in the JSON body, not only HTTP status.pt_co_share_details +
+ policy_transaction), and persist matched brokerage amounts into
+ co_share_stmt_details. The admin UI is
+ app/Views/insurer_statement_list.php; all server logic lives in
+ PolicyTransactionController under the policy_tranction/statement route group.
+authMVC filter. Upload and list are browser AJAX/form calls from an authenticated session, not public API endpoints.
+ Overview
+
+Row validation logic
+
+validateInsurerStatement() checks that every Excel line maps to a real NHance transaction
+ for the selected insurer branch. Each row is matched on policy number (column B) and
+ endorsement number (column C).
+
+
+
+failed and errors are shown per row in the UI.Key files and routes
+
+
+
+
+
+
+
+
+ Area Location
+
+ UI
+
+ app/Views/insurer_statement_list.php — DataTable list, upload modal, validation error modal, invoice modal
+
+ Controller
+
+ app/Controllers/PolicyTransactionController.php
+
+ Statement header model
+
+ app/Models/InsurerStatements.php → insurer_statements
+
+ Line-item model
+
+ app/Models/COShareStmtDetailsModel.php → co_share_stmt_details
+
+ NHance source rows
+
+ app/Models/PTCOShareDetailsModel.php → getNonReconcileredPolicyTransactionByPolicyAndEndorsement()
+
+
+Sample Excel
+
+ public/sample_excel/insurer_stament_sample.xlsxpolicy_tranction/statement, filter authMVC):
+
+
+
+
+
+
+ Method Route Handler
+ GET liststatementList
+ POST uploaduploadInsurerStatement
+ GET downloadSampleInsurerStatementSample file download
+ GET downloadInsurerStatement/(:num)Uploaded file download
+ GET getFileErr/(:any)Validation failure JSON for modal
+ GET getInsurerStatementMonthUsed to disable already-used statement numbers
+ GET deleteStatement/(:any)Soft-delete statement + related rows
+ GET getPaymentDetails/(:any)Invoice modal data
+
+POST saveInvoicePaymentDetailsInvoice / payment save
+
+$routes->group('policy_tranction', ['filter' => 'authMVC'], function ($routes) {
+ $routes->group('statement', ['filter' => 'authMVC'], function ($routes) {
+ $routes->get('list', 'PolicyTransactionController::statementList');
+ $routes->post('upload', 'PolicyTransactionController::uploadInsurerStatement');
+ // ...
+ });
+});Statement list UI
+
+statementList() loads insurers/branches via
+ insurerBranchModel::getInsurerBranchesWithInsurerNames(), invoice status labels, and
+ statements from the last 180 days (is_active = 1). The view shows:
+
+
+
+file_status — success or failed (failed rows show an alert icon → error modal)invoice_status — pending / generated / sent / payment received#insurer_statement_upload_form): insurer
+ (insurer_id-branch_id), statement month (flatpickr), statement no (1–7), Excel file.
+ Submit is AJAX POST to relative upload. On success the page reloads; on validation failure
+ the API still returns HTTP 200 with dataStatus: false and error_data.
+uploadInsurerStatement()
+
+
+
+
+max_size[statement,16384] KB).WRITEPATH . 'uploads/statements/' (see createStatementFolder() for folder creation).insurer as {insurer_id}-{branch_id}, statement_month (converted to first-of-month Y-m-d), statement_no → stmt_sno.insurer_statements row via InsurerStatements model.validateInsurerStatement(['file_id' => $file_id]).updateInsurerStatement(['file_id' => $file_id]).dataStatus: false, error_data, error_code (HTTP 200).invoice_status = 'pending' and returns dataStatus: true.validateInsurerStatement($params)
+
+file_id in statementList() comment).Steps
+
+
+
+
+insurer_statements by file_id; fail if missing or physical file absent under writable/uploads/statements/.ExcelSanitizeHelper::sanitizeArrayData().1), skipping empty rows via check_row_is_empty_or_null().PTCOShareDetailsModel::getNonReconcileredPolicyTransactionByPolicyAndEndorsement(insurer_id, branch_id, policy_no[])
+ — matches pt.policy_no for completed transactions on that insurer branch.policy_no|endorsement_no (sanitized) for source and Excel rows.$matched_entry.error_data[row_index].insurer_statements.line_items, file_status (success / failed), reason (JSON).sanitizeStatementLookupValue()
+
+Validation error codes
+
+
+
+
+
+
+
+
+ error_code Meaning UI
+ 0DB row missing or physical file not found Plain message in modal
+ 1Legacy: list of row numbers (old format) Comma-separated row list
+
+2Row-wise validation (current) Modal lists each row with policy / endorsement / duplicate messages error_data are the array index from the Excel loop (first data row is typically 1 after header removal), not necessarily the Excel row number on sheet.updateInsurerStatement($params)
+
+status: true.
+
+
+pt_co_share_details rows).
+
+ 3, brokerage col 54, brokerage col 60 in current codereward from col 7variance = exp_amt - total_amt (from source exp_amt)COShareStmtDetailsModel::insertBatch($data_to_update) — one row per matched Excel line.file_status = success, invoice_status = pending, clears/sets reason.Excel column mapping (0-based index)
+
+
+
+
+
+
+
+
+ Index Column Use
+ 1B Policy number (required for matching)
+ 2C Endorsement number
+ 3D Actual BP amount
+ 4E Actual TP amount
+ 5F Actual BP brokerage
+ 6G Actual TP brokerage
+
+7H Reward downloadSampleInsurerStatement.NHance source query
+
+getNonReconcileredPolicyTransactionByPolicyAndEndorsement() joins:
+
+
+pt_co_share_details (active) → policy_transaction (active, status = completed)insurer_id, insurer_branch_id, and pt.policy_no IN (...)policy_no + endorsement_no. The method name suggests
+ “non-reconciled” but the current query does not filter statement_id IS NULL;
+ be aware when re-uploading or debugging duplicate reconciliation.
+Invoice status and delete
+
+
+
+
+invoice_status: pending, generated, sent, payment_receivedsaveInvoicePaymentDetails — JSON POST from invoice modaldeleteStatement($id) — soft-deletes co_share_stmt_details, inv_payment_details, and insurer_statements for that idgetInsurerStatementMonth returns successful statements for insurer+month so the UI can
+ disable statement numbers already used (disableStatementNo() in the view).
+Developer steps
+
+
+
+
+writable/uploads/statements/ exists and is writable (or call createStatementFolder() once).policy_tranction/statement/list in a logged-in session.getFileErr/{id} and renders reason JSON.validateInsurerStatement / updateInsurerStatement one-liner in statementList() with a known file_id.Common pitfalls
+
+
+
+
+sanitizeStatementLookupValue(); re-type values if NHance shows a match but upload fails.error_code 2, duplicate message on second row.insurer_statements row remains; user sees failed status; re-upload needs a new statement or delete the failed row.getInsurerStatementMonth.dataStatus, not status code alone.validateInsurerStatementOld, updateInsurerStatementOLD remain in the controller; production path is the non-Old methods documented here.Related BDS features
+
+
+
diff --git a/app/Views/docs/correction.php b/app/Views/docs/correction.php
new file mode 100644
index 00000000..d8d68029
--- /dev/null
+++ b/app/Views/docs/correction.php
@@ -0,0 +1,265 @@
+
+
+policy_tranction/report/list, variance, finance, outstanding listscronDailyBDSReport (separate from statement upload)files.action = correction). The final step creates pending correction endorsements
+ on the employees table — data is not updated until those endorsements are applied downstream.
+EmployeeServiceController::employeesCorrectionProcess, queued after the same
+ format and data validation steps used for inception and deletion.
+Overview
+
+
+
+
+d-M-Y.Key files and routes
+
+
+
+
+
+
+
+
+ Area Location
+
+ UI
+
+ app/Views/employee_upload.php — action Correction
+
+ Upload
+
+ EmployeeController::employeesUplodWithEvents
+
+ Validation
+
+ excelFileFormatValidation, excelFileDataValidation
+
+ Correction process
+
+ EmployeeServiceController::employeesCorrectionProcess
+
+ Column config API
+
+ getCorrectionExcelColumns() — used when building correction Excel programmatically
+
+ Job
+
+ employeesCorrectionProcess in JobWorker.php
+
+
+Endorsements
+
+ EmpEndorsementModel — actions = c, table_name = employees, status = pending/employee, authMVC):
+
+
+GET /employee/upload — upload screenPOST /employee/upload — upload-action-type=correctionGET /employee/excel_error/{file_id} — validation errorsfiles.policy_id is the client policy id. Lookup requires active
+ employees + employee_polices on that policy and branch.
+ Sync vs background jobs
+
+
+
+
+
+
+
+
+ Step < 1 MB ≥ 1 MB
+ Format validation Inline on upload Job excelFileFormatValidation
+ Data validation Job excelFileDataValidationSame
+
+Correction process Job employeesCorrectionProcessSame Step 1: excelFileFormatValidation
+
+
+
+
+$correction_excel_columns — 8 columns (A–H).C for mandatory rules on correction-specific columns.name, dob, relationship, email_corporate.d-M-Y.Format error codes
+
+
+
+
+
+
+
+
+ Code Meaning
+ 1 Mandatory missing
+ 2 Wrong format
+ 3 Not in allowed list (e.g. invalid Field value)
+ 4 Custom validation failed
+ 5 File / policy problem
+
+6 Column headers wrong Step 2: excelFileDataValidation
+
+name_and_empid_check_in_db:
+
+
+employeesCorrectionProcess (with optional batch_file_id for TPA multi-file flows).Step 3: employeesCorrectionProcess
+
+
+
+
+field_name ← column D (name, dob, relationship, email_corporate)new_value ← column E; if field is dob, converted from d-M-Y to Y-m-ddate_of_correction ← column F (converted to Y-m-d)remarks ← column H (optional)old_value ← current value from employees.{field_name}emp_code, name, and field_name
+ (actions = c, endorsement_id IS NULL, status != truncated).
+files.status = success when the loop completes and sends a success pull notification.
+ Rows with no DB match are not endorsed (no row-level failure on the file record).
+batch_file_id is set, also queues
+ updateEmployeeDataFromTpa, reconTpaApiDataWithEmployeepolicies, and
+ initializeDeletionProcessForTpaApiData.
+Correction Excel columns
+
+
+
+
+
+
+
+
+ Col Header Required Notes
+ A S.No Yes
+ B EMP ID Yes
+ C NAME OF EMP/DEP Yes Must match DB before correction
+ D Field Yes name, dob, relationship, email_corporate
+ E Value Yes New value; DOB as d-M-Y
+ F Date of Correction Yes d-M-Y
+ G Change event Yes e.g. correction
+
+H Remarks No Stored on endorsement Row layout examples
+
+
+
+
+
+
+
+
+ EMP ID NAME Field Value Date of Correction Change event Remarks
+
+
+EMP001
+ Raj Kumar
+ dob
+ 15-Jan-1985
+ 19-May-2026
+ correction
+ Typo in upload
+
+
+
+
+
+
+
+ EMP ID NAME Field Value
+ EMP001 Raj Kumar email_corporate raj.kumar@company.com
+
+EMP001 Priya Kumar relationship Spouse Developer steps
+
+
+
+
+upload-action-type=correction; note file_id./employee/excel_error/{file_id} for code 10.emp_endorsement where file_id = upload id, actions = 'c', status = 'pending'.field_name, old_value, new_value per row to the Excel.getCorrectionExcelColumns() for header layout.Common pitfalls
+
+
+
+
+emp_code + name; rename via a name field row uses the old name in column C.files.status = success does not mean every row created an endorsement.employees columns change only after endorsement approval/application.files.action = deletion). Unlike inception, the final step does not delete rows immediately —
+ it creates pending endorsement records on employee_polices for approval/processing later.
+EmployeeServiceController::employeeDisembark.
+ EmployeeController::initializeDeletionProcessForTpaApiData is a separate TPA-reconcile path that
+ builds a deletion Excel file and calls the same disembark function.
+Overview
+
+
+
+
+D.Key files and routes
+
+
+
+
+
+
+
+
+ Area Location
+
+ UI
+
+ app/Views/employee_upload.php — action Deletion
+
+ Upload
+
+ EmployeeController::employeesUplodWithEvents
+
+ Validation
+
+ EmployeeServiceController::excelFileFormatValidation, excelFileDataValidation
+
+ Deletion process
+
+ EmployeeServiceController::employeeDisembark
+
+ TPA auto-deletion
+
+ EmployeeController::initializeDeletionProcessForTpaApiData
+
+ Job
+
+ employeeDisembark in JobWorker.php
+
+
+Endorsements
+
+ EmpEndorsementModel → emp_endorsement (actions = d, status = pending)/employee, authMVC):
+
+
+GET /employee/upload — upload screenPOST /employee/upload — upload-action-type=deletionGET /employee/excel_error/{file_id} — read files.reason after validation failurefiles.policy_id is the client policy id. Member lookup joins
+ employees + employee_polices on that policy and branch.
+ Sync vs background jobs
+
+
+
+
+
+
+
+
+ Step < 1 MB ≥ 1 MB
+ Format validation Inline on upload Job excelFileFormatValidation
+ Data validation Job excelFileDataValidationSame
+
+Disembark Job employeeDisembarkSame Step 1: excelFileFormatValidation
+
+
+
+
+$deletion_excel_columns — 7 columns (A–G).D drives mandatory fields: Change event, Date of exit, Reason for exit, Claim status.d-M-Y.0 or 1.Format error codes
+
+
+
+
+
+
+
+
+ Code Meaning
+ 1 Mandatory missing
+ 2 Wrong format
+ 3 Not in allowed list
+ 4 Custom validation failed
+ 5 File / policy problem
+
+6 Column headers wrong Step 2: excelFileDataValidation
+
+name_and_empid_check_in_db:
+
+
+employee_polices row on the uploaded policy/branch.employeeDisembark (not employeesOnboardPreprocess).Step 3: employeeDisembark
+
+employee_polices:
+
+
+date_of_exit ← Excel column E (converted to Y-m-d)reason_for_exit ← column Fstatus → inactiveclaim_status ← column Gemployee.id values. Sets files.status = success when the loop finishes
+ (even if some rows were skipped — check logs for “not found” or “existing endorsement pending”).
+TPA auto-deletion (EmployeeController)
+
+initializeDeletionProcessForTpaApiData($file_id):
+
+
+tpa_api_data, action_flag_status = D).writable/uploads/excel/tpa_auto_deletion_{fileId}_{timestamp}.xls.files row with action = deletion.employeeDisembark(['file_id' => $newFileId]) synchronously.getDeletionEmployeeDataForExportExcel.Deletion Excel columns
+
+
+
+
+
+
+
+
+ Col Header Required Notes
+ A S.No Yes
+ B EMP ID Yes Employee / family code
+ C NAME OF EMP/DEP Yes Must match DB name exactly
+ D Change event Yes e.g. deletion
+ E Date of exit Yes d-M-Y
+ F Reason for exit Yes
+
+G Claim status Yes 0 or 1Row layout examples
+
+
+
+
+
+
+
+ Row EMP ID NAME Change event Date of exit Reason Claim
+
+2 EMP001 Arjun Kumar deletion 19-May-2026 Resigned 0
+
+
+
+
+
+ Row EMP ID NAME Change event Date of exit Reason Claim
+
+2 EMP001 Raj Kumar deletion 19-May-2026 Resigned 0 Developer steps
+
+
+
+
+/employee/upload with action deletion; note file_id.GET /employee/excel_error/{file_id} — look for code 10 (member not in DB).emp_endorsement where file_id = upload id, actions = 'd', status = 'pending'.not found or Existing endorsement pending.initializeDeletionProcessForTpaApiData and the generated tpa_auto_deletion_*.xls file.Common pitfalls
+
+
+
+
+employees.name exactly (case/spacing).emp_status = active and employee_polices.status = active match.files.status does not reflect per-row skips; use endorsements table + logs.
@@ -321,13 +294,122 @@ flowchart LR
(already grouped to one employee). Keys align with the JSON used in rack configuration (get_familiy_compositionadditional_relationship), except
either-parents-pil and elders_count which are stripped before comparison.
+ This function mainly: +
+ Inception here means onboarding employees and dependents from an Excel upload
+ (files.action = inception or related actions like missed_inception,
+ addition, dependent_addition). The same three-step pipeline runs for those
+ actions; this page focuses on the inception path.
+
+ Manual policy inception (form UI under policy_tranction/inception) is a separate flow
+ in PolicyTransactionController — not covered by these three functions.
+
In short:
+JobWorker.| Area | Location |
|---|---|
| UI | +app/Views/employee_upload.php — client, branch, policy, action Inception, file upload |
+
| Upload handler | +EmployeeController::employeesUplodWithEvents |
+
| Pipeline logic | +EmployeeServiceController — the three functions on this page |
+
| Job dispatch | +app/Controllers/JobWorker.php — maps job names to EmployeeServiceController |
+
| Column / Excel helpers | +app/Helpers/excel_util_helper.php — check_columns_name, custom validators |
+
| Upload record | +files table via FileModel — status, action, reason |
+
| Premium | +EB rack rate calculation — calculate_premium_new, employeesOnboardProcess |
+
Routes (group /employee, filter authMVC):
GET /employee/upload — upload screenPOST /employee/upload — upload + start validation (upload-action-type=inception)GET /employee/excel_error/{file_id} — returns files.reason JSON for the error modal
+ REST upload (mobile/HR): POST employeeRest/employeeUpload — same pipeline via
+ EmployeeRestController.
+
files.policy_id stores client policy id (client_policies.id),
+ not the insurer policy master id. Slab/rack lookups use this id with client_id.
+ | File size | Step 1 | Steps 2–3 |
|---|---|---|
| < 1 MB | +excelFileFormatValidation runs inline in the upload request |
+ Always queued: excelFileDataValidation → employeesOnboardPreprocess |
+
| ≥ 1 MB | +Job excelFileFormatValidation |
+ Same job chain after format passes | +
+ After upload the UI usually shows files.status = inprogress until jobs finish.
+ Poll notifications or refresh the upload list; use /employee/excel_error/{id} when status is failed.
+
Job chain for inception (and missed_inception / addition / dependent_addition):
+excelFileFormatValidation → excelFileDataValidation → employeesOnboardPreprocess
Runs on the uploaded sheet before any DB business rules.
+ +writable/uploads/excel/{file_name}.$inception_excel_columns when action is inception (same column set for addition / dependent_addition / missed_inception).I), date/mobile formats, allowed lists, custom helpers (DOB, relationship, SI, mobile duplicate, etc.).| Code | Meaning |
|---|---|
| 1 | Mandatory value missing |
| 2 | Wrong format (e.g. date, mobile) |
| 3 | Value not in allowed list |
| 4 | Custom validation failed (DOB, relationship, SI, etc.) |
| 5 | File / policy / slab configuration problem |
| 6 | Column headers wrong or out of order |
On failure: files.status = failed, reason JSON with row/column errors; user notification via pull notification.
Lead-policy branch: If inception file is for a policy created from leads (policy_entry_from == 3 and is_from_lead set), format validation queues compareMemberDataAndInceptionData instead of going straight to data validation.
Stored as JSON string on files.reason. Typical failure payload:
{
+ "error_type": 1,
+ "error_summary": { "4": 2, "1": 1 },
+ "error_data": {
+ "3": {
+ "dob": { "error": ["Invalid date format"], "value": "01/01/1990" }
+ }
+ }
+}
+error_type — 1 = format step, 2 = data steperror_summary — counts per error code (after aggregation)error_data — keyed by Excel row number (1-based, header is row 1)Runs after format passes. Groups rows by EMP ID (family) and applies business rules.
+ +Typical inception checks:
+policy_terms (family composition, LGBTQ flag, etc.).name_and_empid_check_in_db).On success for inception: queues job employeesOnboardPreprocess. Other actions queue different jobs (deletion, correction, etc.).
| Code | Meaning |
|---|---|
| 7 | Duplicate name within same family in the Excel file |
| 9 | Record already exists in DB (inception / addition) |
| 10 | Record not found (used on deletion flows) |
| 14 | Self row missing in family |
| 26 | Duplicate employee code in file or DB |
Calculates premium and writes members to the database.
+ +emp_id.calculate_premium_new() using policy terms + slab rates + rack config.employeesOnboardProcess() — insert/update employees, employee_polices, create policy_transaction (inception).files.status = success; else failed with rack-rate message.
+ Optional second path: client_policy_id without file_id — converts enrolled DB members
+ to inception (enrollment → inception), not from Excel.
+
+ TPA batch: When batch_file_id is present on the job payload, success also queues
+ updateEmployeeDataFromTpa, reconTpaApiDataWithEmployeepolicies, and
+ initializeDeletionProcessForTpaApiData (multi-file TPA reconcile flow).
+
+ Defined in EmployeeServiceController::$inception_excel_columns.
+ Header row must match exactly — 19 columns (A–S), row 1 only.
+ Dates use format d-M-Y (e.g. 4-Apr-1990).
+ One Self row per EMP ID; other rows are dependents.
+
| Col | Header | Required (inception) | Notes |
|---|---|---|---|
| A | S.No | Yes | |
| B | EMP ID | Yes | Family key |
| C | NAME OF EMP/DEP | Yes | |
| D | DOB | Yes | d-M-Y; age vs relationship checked |
| E | Gender | Yes | M / F (several casings allowed) |
| F | RELATIONSHIP | Yes | Self, Spouse, Son, Daughter, … |
| G | BASIC COVER SI | Conditional | Validated against slab when applicable |
| H | Date of Coverage | No | Mandatory for addition / DA only |
| I | DOJ | No | |
| J | Basic Pay | No | Used when policy terms need it |
| K | Band/Grade | No | |
| L | Designation | No | |
| M | Phone | No | Mobile format; duplicate check |
| N | No | Duplicate check in file | |
| O | PRE EXISTING AILMENTS | Yes | 0 or 1 |
| P | Change event | No | Not used for pure inception |
| Q | Date of exit | No | Deletion only |
| R | Reason for exit | No | Deletion only |
| S | Unit | No | Must match branch units when filled |
Sample file: use the download link on the employee upload screen (environment-specific).
+ ++ All rows with the same EMP ID (column B) are treated as one family. + Step 2 requires exactly one Self row in that group; dependents share the same EMP ID. + Step 3 runs premium and DB insert once per family. +
+ +Example: one employee (EMP001) with spouse and son — three data rows plus header.
| Row | +S.No | +EMP ID | +NAME | +DOB | +Gender | +RELATIONSHIP | +BASIC COVER SI | +PED | +
|---|---|---|---|---|---|---|---|---|
| 1 | Header row (all 19 columns A–S required in file) | |||||||
| 2 | +1 | +EMP001 | +Raj Kumar | +15-Jan-1985 | +M | +Self | +500000 | +0 | +
| 3 | +2 | +EMP001 | +Priya Kumar | +20-Mar-1988 | +F | +Spouse | +500000 | +0 | +
| 4 | +3 | +EMP001 | +Arjun Kumar | +10-Jun-2015 | +M | +Son | +500000 | +0 | +
Rules illustrated:
+d-M-Y; ages are checked against relationship (e.g. Son vs Self).0 or 1 on every member for inception.
+ A second employee in the same file uses a different EMP ID (e.g. EMP002) with its own Self row — each EMP ID is a separate family loop in preprocess.
+
policy_terms JSON and slab/rack rates for the client policy./employee/upload with action inception; note file_id in response or files table.status = failed, call GET /employee/excel_error/{file_id} or read files.reason.error_data row keys back to Excel (row 1 = header).calculate_premium_new (see rack-rate doc) and SI/slab config.inprogress, check job queue / JobWorker logs for the three job names.policy_terms or slab rates fails step 1 with code 5.calculate_premium_new returns data for every family.files.reason JSON for row-level errors after failure.+ This page documents the Non-EB Claims web workflow: list and filter claims, + create and edit tickets, status-driven form sections, document uploads, manual email reply, + mail template CRUD, auto-mail on create/status change, and claim reports. +
+ +
+ Scope: authenticated MVC routes under /non-eb-claim/* only.
+ Mobile/REST endpoints in Api\NonEbClaimApiController are not covered here.
+
+ Policy types are limited to policy_type.allocg IN ('Non-EB', 'Marine').
+ Claim records live in non_eb_ticket_master; claim files use ticket_type = 2
+ in claim_files.
+
Typical user path:
+50 — see why 50 is hardcoded.is_auto_mail = 1.ticket_claim_status.allowed_status; section visibility updates./non-eb-claim/mail_template (direct URL; not in main Claims sidebar today).50 is hardcoded (read this before changing Non-EB Claims)
+ Non-EB has many real policy products (Fire, Marine, Liability, etc. — each row in
+ policy_type with allocg Non-EB or Marine). Claim status workflows are
+ not stored separately per product today. The team configured one canonical policy type,
+ id 50, in the database as the single source of status definitions. The application
+ assumes every Non-EB/Marine claim uses that same status set unless you deliberately change
+ backend and frontend.
+
+ Claim statuses live in ticket_claim_status, keyed by ticket_type (which equals
+ policy_type.id). Mail templates in ticket_mail_template also key off
+ ticket_type + trigger_type from that status row.
+
Instead of maintaining duplicate status trees for every Non-EB product, developers assumed:
+ +++ ++ All Non-EB and Marine policy types share one identical claim status lifecycle. + That lifecycle is configured only under policy type
+50in +ticket_claim_status(and matching templates underticket_type = 50). +
+ New claims opened via the list Add button or New Non EB Claim menu therefore
+ pass 50 into the form URL. Status dropdowns, section visibility, and initial status resolution in
+ getClaimStatusForPolicyType($policy_type_id) all use that id on create — not the id of the policy
+ the user later picks from client_policy.
+
On create (non_eb_claim_form.php):
policy_type_id is set from the URL segment (typically 50) and is not updated when the user selects a branch policy.policy_type_display from the selected policy row (data-policy-type).POST /non-eb-claim/create persists policy_type_id from that hidden field — so new tickets from Add often store policy_type_id = 50 even when the linked policy is Fire, Marine, etc.
+ On edit (non_eb_claim_edit.php), policy_type_id comes from
+ non_eb_ticket_master as saved. Status changes and templates use that stored id.
+
50 appears in code (change all if this design changes)| Location | Usage |
|---|---|
app/Config/Routes.php | GET non-eb-claim/new → claimForm/50 |
app/Views/non_eb_claim_list.php | DataTable Add button → /non-eb-claim/new/50 |
app/Views/non_eb_claim_form.php | Hidden policy_type_id from route; status/section AJAX uses this value |
ticket_claim_status (DB) | Master status rows maintained with ticket_type = 50 |
ticket_mail_template (DB) | Non-EB auto-mail templates should use ticket_type = 50 if they follow the shared workflow |
Api\NonEbClaimApiController::listClaimStatuses() | API hardcodes where('ticket_type', 50) (out of scope for this page but same assumption) |
+ If a new (or existing) policy type must have its own statuses, triggers, or allowed transitions
+ (not the shared tree under 50), you cannot only change the product row in
+ policy_type. You must update both backend and frontend:
+
ticket_claim_status rows with
+ ticket_type = <that policy_type.id> (claim_status, display_name, trigger_type,
+ allowed_status JSON). Add matching ticket_mail_template rows for that
+ ticket_type if auto-mail applies.50: e.g. change
+ claimForm/50, restore policy-type picker modal (commented in
+ non_eb_claim_search.php), or pass the correct policy_type_id per product.new/50 in
+ non_eb_claim_list.php with the correct id or dynamic selection.#policy_type_id to the real
+ policy_type_id from getBranchAndPolicy (field p.policy_type_id is
+ already returned) so create/update and getVisibleSections use the right status tree.getClaimStatusForPolicyType,
+ getTemplateDataByTicketID, and filters that assume a single Non-EB status catalog are tested for
+ the new ticket_type.50 in listClaimStatuses() if mobile
+ clients need per-product statuses.+ Until those steps are done, pointing Add at another id without cloning the full status + template set under + that id will produce empty status lists, wrong section visibility, or + missing auto-mail. +
+ +| Area | File / route |
|---|---|
| Controller | app/Controllers/NonEbClaimController.php |
| Model | app/Models/NonEbTicketMasterModel.php |
| List + filters | app/Views/non_eb_claim_search.php, non_eb_claim_list.php |
| New claim | app/Views/non_eb_claim_form.php |
| View / edit | app/Views/non_eb_claim_edit.php |
| Mail templates | app/Views/non_eb_claim_mail_template.php |
| Reports | app/Views/non_eb_claim_reports.php |
| Routes | app/Config/Routes.php — group /non-eb-claim, filter authMVC |
| ACL | app/Config/Acl.php — #^/non-eb-claim# (Claims team roles) |
| App menu | app/Views/layout/header.php — New / List only (EB mail template link is separate) |
| Method | Route | Controller | Purpose |
|---|---|---|---|
| GET | /non-eb-claim/list | claimList | Search page + default open claims |
| POST | /non-eb-claim/list | claimList | Filter → HTML partial for DataTable |
| GET | /non-eb-claim/new | claimForm/50 | New claim (default policy type 50) |
| GET | /non-eb-claim/new/{policy_type_id} | claimForm | New claim for selected product |
| POST | /non-eb-claim/create | createClaim | Create ticket + assets + history + auto-mail |
| GET | /non-eb-claim/view/{id} | view_claim | Edit layout (non_eb_claim_edit) |
| POST | /non-eb-claim/update | updateClaim | Update; auto-mail on status change |
| GET | /non-eb-claim/remove?ticket_id= | removeClaim | Soft delete (is_active = 0) |
| GET | /non-eb-claim/mail_template | mailTemplate | Template list + modal CRUD UI |
| POST | /non-eb-claim/crud_mail_template/1 | crudTemplate | Save template |
| POST | /non-eb-claim/crud_mail_template/2 | crudTemplate | Fetch one template (edit) |
| POST | /non-eb-claim/crud_mail_template/3 | crudTemplate | Soft delete template |
| POST | /non-eb-claim/note/1 | crudNote | Get note |
| POST | /non-eb-claim/note/2 | crudNote | Save note |
| POST | /non-eb-claim/reply | saveReply | Manual outbound mail + message row |
| GET | /non-eb-claim/reports | claimReports | Reports UI shell |
| POST | /non-eb-claim/reports | claimReports | Not implemented in controller — see Reports |
| POST | /non-eb-claim/getBranchAndPolicy | getBranchAndPolicyByClientID | Branches, policies, contacts for client |
| POST | /non-eb-claim/getVisibleSections | getVisibleSectionsAjax | Section keys for status |
| POST | /non-eb-claim/getMoreInfo | getMoreInfo | Ticket row JSON |
| POST | /non-eb-claim/uploadFile | uploadFile | Claim docs (file or Drive URL) |
| POST | /non-eb-claim/getClaimFiles | getClaimFiles | List files for ticket |
| GET | /non-eb-claim/removeFile?id= | removeFile | Soft delete file |
| POST | /non-eb-claim/saveIRDocs | saveIRDocs | Persist required-docs JSON on ticket |
| GET | /non-eb-claim/testAutoMail/{id} | testAutoMailTrigger | Dev/test auto-mail (optional) |
+ All routes in the /non-eb-claim group use the authMVC filter.
+ Acl.php allows roles HEAD, ADMIN, MANAGER,
+ ACCOUNT_MANAGER on the Claims team.
+
+ List row actions (view / delete) render only for roles 1, 2, 5 in
+ non_eb_claim_list.php.
+
GET /non-eb-claim/list loads non_eb_claim_search.php, which includes the table partial. Initial data comes from claimSearch(1): active tickets where status display name is not in Claim Settled, Claim Closed, Claim Rejected, Claim Withdrawn.
Filter sidebar posts the same URL with criteria (at least one required):
+policy_type_id, insurer_id, claim_number, nhance_claim_ref_noclient_id, claim_status_id (status options filtered by policy type in JS)date_type + start_date / end_date (created_date or updated_date)
+ Response is JSON { status: true, html: "…" }; JS replaces #claim_list_div
+ and re-initializes the DataTable. Row click navigates to /non-eb-claim/view/{id}.
+
+ Add button: window.location.href = '/non-eb-claim/new/50'.
+ Header menu uses /non-eb-claim/new (routes to claimForm/50).
+ See Policy type 50 for why this id is fixed and what to change if a product needs its own statuses.
+
GET /non-eb-claim/new/{policy_type_id} → getFormData(): ACMs (role 3), clients, insurers, initial claim status for product, visible sections.POST getBranchAndPolicy fills branch, policy (Non-EB/Marine only), branch contact.POST getVisibleSections toggles accordion sections client-side.POST /non-eb-claim/create — validation via getValidationRules(), sanitizeInputArrayAdvanced, date normalization, optional asset file upload.client_id + loss_date + policy_no (if policy set) → HTTP 409-style JSON.non_eb_ticket_master, saveAssets(), history row, sendAutoMailTrigger(), redirect to list.
+ view_claim($id) loads ticket via NonEbTicketMasterModel::getTicketDataByTicketID(),
+ merges mail template preview (getTemplateDataByTicketID + placeholder replace),
+ messages, history, assets, and reuses the edit view non_eb_claim_edit.php.
+
+ POST /non-eb-claim/update mirrors create validation. If claim_status_id changes,
+ auto-mail runs again. remark_mode=append appends to closure_remark with a separator.
+
+ $statusSectionVisibility in the controller maps each ticket_claim_status.claim_status
+ label to section keys: policy_account, loss_incident, intimation,
+ insured_contact, asset, documents, surveyor, settlement.
+
+ Allowed next statuses come from getClaimStatusForPolicyType(): current status plus IDs in
+ allowed_status JSON on the status row. Both create and edit forms call
+ getVisibleSectionsAjax when the user changes status.
+
Template lookup (NonEbTicketMasterModel::getTemplateDataByTicketID):
ticket_claim_status on ticket’s claim_status_id (or explicit status_id for tests).ticket_mail_template where ticket_type = policy_type_id AND trigger_type = tcs.trigger_type.
+ sendAutoMailTrigger($ticket_id) sends only when the matched template has
+ is_auto_mail = 1. Mail goes to insured_contact_email from
+ constructMailContent(); from address claims@nhanceindia.in via MailHelper::send_email().
+ Successful sends insert a ticket_messages row via autoMessageInsertBasedOnMailResponse().
+
Placeholders (subject/body):
+ +| Token | Ticket field |
|---|---|
((ACM)) | acm |
((ACM_CONTACT)) | acm_mobile |
((INSURED_NAME)) | insured_contact_name |
((CORPORATE_NAME)) | client_name |
((CLAIM_NO)) | claim_number |
((POLICY_TYPE)) | policy_type_name |
((NHANCE_REF_NO)) | nhance_claim_ref_no |
((LOSS_DATE)) | loss_date |
((LOSS_LOCATION)) | loss_location |
((NATURE_OF_LOSS)) | nature_of_loss |
+ Edit screen also supports manual reply: POST /non-eb-claim/reply validates To/Subject,
+ inserts ticket_messages, sends via sendReplyMessage() with placeholder replacement.
+
+ Page: GET /non-eb-claim/mail_template. DataTable lists rows from
+ ticket_mail_template (is_active = 1). Tooltip on each row shows the
+ matching ticket_claim_status.claim_status for that policy type + trigger type.
+
template_name, ticket_type (policy type id), trigger_type (1–9)subject, mail_content (Jodit HTML)is_auto_mail — checkbox “Auto Mail” (1 = send on create / status change when matched)id on save for update
+ Trigger ↔ status: Each ticket_claim_status row for a Non-EB/Marine
+ ticket_type has a trigger_type. The template’s trigger_type must match
+ that column for auto-mail and for the “Claim Status” readonly hint (tool_tip from server on fetch).
+
| Action | Endpoint | Body | Result |
|---|---|---|---|
| Add / Save | POST …/crud_mail_template/1 | Form fields + mail_content + is_auto_mail | { status: bool } → reload |
| Edit load | POST …/crud_mail_template/2 | id | { status, data } opens modal |
| Delete | POST …/crud_mail_template/3 | id | Soft delete (is_active = 0) |
+ Placeholder dropdown in the modal inserts tokens into subject (focused input) or Jodit body.
+ Validation errors return HTTP 400 with errors map (shown via toastr).
+
On the edit screen:
+POST /non-eb-claim/note/1 — id (ticket), optional is_auto_query → fetch active note.POST /non-eb-claim/note/2 — save note (required 3–1000 chars) via TicketNoteModel.uploadFile — multi upload to writable/uploads/claim_files/ or Google Drive URLs (file_type 1 vs 2).getClaimFiles — lists rows; local files expose download URL downloadClaimFile/{id}.removeFile — soft delete by file id.saveIRDocs — stores JSON in non_eb_ticket_master.required_docs.asset_file under writable/uploads/non_eb_asset_files/; loss description required when file uploaded.
+ GET /non-eb-claim/reports renders filters: policy type, ACM name, date range (default last 60 days).
+ generateReport() in the view POSTs to the same URL and expects
+ { status: true, data: [ rows ] } for DataTable columns (status, policy type, claim/ref, client, insurer, loss fields, surveyor, settlement, ACM, created date).
+
+ Gap: NonEbClaimController::claimReports() only handles GET (layout load).
+ There is no POST handler to return report data — Generate Report will fail until POST logic is added
+ (mirror EB ticket_reports or reuse claimSearch with report-specific selects).
+
| Table | Role |
|---|---|
non_eb_ticket_master | Main claim ticket |
non_eb_claim_asset | Repeating asset lines per ticket |
ticket_claim_status | Statuses per ticket_type (policy type id); trigger_type, allowed_status |
ticket_mail_template | Templates; ticket_type = policy type id |
ticket_history | Field-level audit (status, ACM, priority, …) |
ticket_messages | Outbound mail log |
ticket_notes | User notes per ticket |
claim_files | Attachments; ticket_type = 2 for Non-EB |
| Method | Used for |
|---|---|
claimList / claimSearch | List UI and filtered HTML |
claimForm / getFormData | New claim form bootstrap |
view_claim | Edit view |
createClaim / updateClaim | Persist ticket |
getClaimStatusForPolicyType / getVisibleSections* | Status dropdown + sections |
mailTemplate / crudTemplate | Template admin |
sendAutoMailTrigger / constructMailContent | Automated email |
saveReply / getTicketMessage | Manual email thread |
crudNote | Notes |
uploadFile / getClaimFiles / removeFile / saveIRDocs | Documents |
saveAssets / getAssets | Asset grid |
claimHistory / putHistoryAfterInsert | Audit trail |
getBranchAndPolicyByClientID | Client cascade |
claimReports | Reports page (GET only today) |
testAutoMailTrigger | Dev preview/send test |
ticket_claim_status rows exist per Non-EB/Marine policy_type.id with correct trigger_type and allowed_status./non-eb-claim/mail_template with matching ticket_type + trigger_type; enable Auto Mail only where intended.50 if a product needs its own workflow.claimReports() if reports Generate must work.ticket_type = 2 in new file-related code paths.#^/non-eb-claim# if new roles need access.ticket_mail_template for policy type + status trigger_type → no auto-mail and empty reply preview./non-eb-claim/reports but controller only loads view on GET.ticket_type = 50; Add/new routes and hidden form field use 50 — not the policy picked on the form. Different per-product statuses require full backend + frontend changes — checklist./ticket/mail_template, not Non-EB.is_active = 0; list queries filter active only.employeeRest / Api\NonEbClaimApiController — different validation and flows.
+ This page documents the Non-EB Opportunities workflow from the
+ Opportunities list (/leads/list). A user creates a Non-EB opportunity,
+ then progresses through RFQ → QCR → mail actions → placement — all from the list row action menu.
+
+ Non-EB rows are identified by leads.lead_form_type = 2 (EB = 1).
+ RFQ and QCR are managed in Google Sheets; sheet IDs are stored in
+ leads.misc JSON.
+
Typical sequence from the list:
+queuedrfq_createdqcr_createdrfq_sentqcr_sentwon
+ Edit is available at any stage from the same action menu and reuses the add form
+ with pre-filled data (getLeadNonEB + POST /leads/create with lead id).
+
| Area | File |
|---|---|
| List + action dropdown | app/Views/leads_list.php |
| Non-EB add/edit form | app/Views/leads_non_eb.php |
| EB vs Non-EB form include | app/Views/leads_form_handler.php |
| All backend logic | app/Controllers/LeadsController.php |
| Google Sheets | GoogleSheetLib, Config\RfqConfig |
+ Non-EB RFQ, QCR, and Placement sheets are Google Drive files created at runtime by
+ GoogleSheetLib using a service account. Only RFQ needs a pre-configured
+ template per product; QCR and Placement are copies of the lead’s RFQ/QCR sheets.
+
Library: app/Libraries/GoogleSheetLib.php — auth via service account JSON at
+{project-root}/nhance-ee8d1-e3c5269b1ec7.json, scopes DRIVE + SPREADSHEETS.
| Step | Before | Why |
|---|---|---|
| 1 | +Google service account JSON present; account email shared on template folder + each template sheet | +GoogleSheetLib cannot copy or edit without Drive access |
+
| 2 | +RFQ_PARENT_FOLDER_ID set in .env |
+ Target folder for every copied RFQ/QCR/Placement file | +
| 3 | +Master RFQ Google Sheet template created per product (Fire, Marine, GPA, etc.) with placeholder tokens | +RFQ copy source — one template per policy_type row |
+
| 4 | +policy_type.misc JSON updated with rfq_template_sheet_id for that product |
+ createRfqSheet() reads this; fails with “RFQ template not found” if missing |
+
| 5 | +Non-EB opportunity created with correct policy_type_id and rfq_qcr_viewers emails |
+ Lead must exist; viewers become sheet editors after copy | +
| 6 | +Before QCR: RFQ action completed (leads.misc.rfq_sheet_id set) |
+ QCR copies the lead’s RFQ sheet, not the policy template | +
| 7 | +Before Placement mail: QCR action completed (leads.misc.qcr_sheet_id set) |
+ Placement copies the QCR sheet | +
+ Template sheet IDs are stored per product on the policy_type table —
+ one row per product (Fire, Marine, Burglary, etc.). The lead’s selected
+ policy_type_id determines which template is copied when RFQ is clicked.
+
Database column: policy_type.misc (JSON text)
{
+ "rfq_template_sheet_id": "1GXNDNXoWriClb5HCqPYd2GAY1T8aie_Hos0yAOoTaC0"
+}
+
+Example — set or update for policy type id 12:
UPDATE policy_type
+SET misc = JSON_SET(COALESCE(misc, '{}'), '$.rfq_template_sheet_id', 'YOUR_GOOGLE_DRIVE_FILE_ID')
+WHERE id = 12;
+
+rfq_template_sheet_id today — configure via DB (or extend
+ MasterController::editPolicyType / policy_type_onboarding if you add a form field).
+ Model allow-list: app/Models/PolicyTypeModel.php includes misc.
+ How to find a template file ID:
+https://docs.google.com/spreadsheets/d/{FILE_ID}/editphp public/index.php cli/list-sheet-folder-files {RFQ_PARENT_FOLDER_ID}
+ → writes sheetid.json with name + sheetId pairs
+ (GoogleSheetController::listFolderSheetFilesCli)QCR and Placement: no separate template ID per product. They always copy from the lead’s existing sheets:
+ +| Stage | Copy source | Stored on lead |
|---|---|---|
| RFQ | policy_type.misc.rfq_template_sheet_id | leads.misc.rfq_sheet_id |
| QCR | leads.misc.rfq_sheet_id | leads.misc.qcr_sheet_id |
| Placement | leads.misc.qcr_sheet_id (filename: QCR → Placement) | leads.misc.placement_sheet_id |
| Setting | Location | Purpose |
|---|---|---|
RFQ_PARENT_FOLDER_ID |
+ .env → Config\RfqConfig::$rfqParentFolderId |
+ Google Drive folder where copied RFQ/QCR/Placement files are created | +
rfqPlaceholders |
+ app/Config/RfqConfig.php |
+ Maps template tokens like {{INSURED_NAME}} to lead field keys filled on RFQ create |
+
rfqClaimsPlaceholder |
+ app/Config/RfqConfig.php |
+ Default {{CLAIMS_DETAILS}} — multi-row claims table from fin_years_claims |
+
protections |
+ app/Config/RfqConfig.php |
+ Locked cell ranges on new RFQ sheets only (e.g. RFQ Page!B12:C12) |
+
| Service account key | +nhance-ee8d1-e3c5269b1ec7.json (project root) |
+ Google API authentication for all sheet operations | +
Placeholder tokens to embed in each product’s RFQ master template:
+ +| Token in template | Filled from |
|---|---|
{{INSURED_NAME}} | Client name / short name |
{{COMMUNICATION_ADDRESS}} | Client or custom field address |
{{GST}} / {{PAN}} | Lead GST / PAN |
{{POLICY_PERIOD}} | Policy start – end dates |
{{OPPORTUNITY_TYPE}} | Fresh / Renewal label |
{{RISK_LOCATION}} / {{OCCUPANCY}} | Custom policy-type fields |
{{CLAIMS_DETAILS}} | Claim history table (renewal leads) |
+ After RFQ copy, editors are granted from the lead’s rfq_qcr_viewers JSON email list (selected on the create/edit form).
+ The same list is applied to QCR and Placement copies.
+
/leads/list → click Add.lead_form_type=2).GET /util/getLeadNonEB/2/0.leads_non_eb.php)allocg = Non-EB or Marine.GET /util/getPolicyTypeFields → dynamic fields in #appendArea.fin_years_claims JSON.rfq_qcr_viewers (emails) become Google Sheet editors later.POST /leads/create with lead_form_type=2.createLead()prepareLeadData() → prepareSingleLeadData() (one lead row; EB uses multi-row).claim_history=1.insertNewLead() — no demography background job (EB-only).queued (In-Queued).From the list action menu → Edit (available for both EB and Non-EB):
+ +getLeadsDataForEdit(lead_id, lead_form_type, actual_lead_id)GET /util/getLeadNonEB/{lead_form_type}/{actual_lead_id}/{lead_id}getLeadNonEB($type, $actual_lead_id, $id) (~line 4818)
+ $id present: fetches lead_edit_data, files, custom fields, date formattinggenerateViewPageHtml()rfq/claims_details_non_eb into claims_details_htmlleads_form_handler → includes leads_non_eb.php when selected_lead_type != 1POST /leads/create with hidden id → updateOldLead()
+ When lead_form_type === 2, the row action menu in
+ app/Views/leads_list.php (lines ~297–343) uses modal/AJAX flows instead of
+ navigating to /rfq/list/{id}/1|2 (EB behaviour).
+
| # | +Action | +Visible when | +View (JS) / handler class | +Controller endpoint(s) | +Status after | +
|---|---|---|---|---|---|
| — | +Edit | +Always (EB + Non-EB) | +getLeadsDataForEdit() |
+
+ GET
+ /util/getLeadNonEB/{lead_form_type}/{actual_lead_id}/{lead_id}+ POST + /leads/create (update when id posted)
+ |
+ User-selected on form | +
| 1 | +RFQ | +lead_form_type === 2 only |
+ .btnRfqSheetList → createRfqSheetFromList() |
+
+ GET
+ /leads/createRfqSheet?lead_id={id}+ LeadsController::createRfqSheet() — opens Google Sheet URL in new tab
+ |
+ rfq_created |
+
| 2 | +QCR | +
+ Non-EB; status not queued or rfq_created;
+ role 1, 5, 2, 3 or Business Support team
+ |
+ .btnQcrSheetList → createQcrSheetFromList() |
+
+ GET
+ /leads/createQcrSheet?lead_id={id}+ LeadsController::createQcrSheet()
+ |
+ qcr_created |
+
| 3 | +Send Internal Mail | +Non-EB; status != queued |
+ .btnInternalMailList → openInternalMailFromList() |
+
+ GET
+ /leads/mailTemplate?lead_id={id}&template_type=rfq+ POST + /leads/sendMail — recipient_type=internal
+ |
+ — | +
| 4 | +Send Insurer Mail | +Same as internal mail | +.btnInsurerMailList → openInsurerMailFromList() |
+
+ GET
+ /leads/mailTemplate?lead_id={id}&template_type=rfq+ POST + /leads/sendMail — recipient_type=insurer (RFQ sheet attach)
+ |
+ rfq_sent |
+
| 5 | +Send Client Mail | +Same as internal mail | +.btnClientMailList → openClientMailFromList() |
+
+ GET
+ /leads/mailTemplate?lead_id={id}&template_type=qcr+ POST + /leads/sendMail — recipient_type=client (QCR sheet attach)
+ |
+ qcr_sent |
+
| 6 | +Placement | +Same as internal mail | +.btnPlacementList → openPlacementFromList() |
+
+ GET
+ /rfq/placementData/{id}+ GET + /leads/mailTemplate?lead_id={id}&template_type=placement+ POST + /leads/sendMail — recipient_type=placement
+ |
+ won |
+
| — | +Email History | +Always (EB + Non-EB) | +.btnHistory → getLeadsDataForMailHistory() |
+
+ GET
+ /util/getLeadEmailHistory/{id} → getLeadEmailHistory()
+ |
+ — | +
EB contrast (same menu, lead_form_type === 1): RFQ/QCR are links to
+/rfq/list/{id}/1 and /rfq/list/{id}/2; internal/insurer/client/placement mail items are not shown.
Shared mail modal helpers (all in leads_list.php):
POST /leads/uploadLeadAttachment — optional extra files (lead_id, docs_name, file)constructURL_ForInternalMailSend(), insurer/client via constructURL_ForInsurerOrClientMailSend(), placement via constructURL_ForPlacementMailSend()#external_cc_disclaimer_modal) before client/placement send to non-user emailscreateRfqSheet())policy_type.misc.rfq_template_sheet_id.misc.rfq_sheet_id exists → return existing Google Sheet URL.GoogleSheetLib::copyTemplate() into RfqConfig::rfqParentFolderId.buildRfqPlaceholderData()): client, GST, PAN, policy period, claims table.leads.rfq_qcr_viewers email JSON.misc.rfq_sheet_id; set status = rfq_created.createQcrSheet())misc.rfq_sheet_id — returns 400 if RFQ not created yet.misc.qcr_sheet_id exists → return existing URL.rfq_qcr_viewers.misc.qcr_sheet_id; set status = qcr_created.getLeadMailTemplate()
+ Called before each mail modal opens. Query params:
+ lead_id + template_type (rfq | qcr | placement).
+ Returns subject, HTML body, and attachment checkbox HTML from lead_files.
+
sendMailWithAttachement()For Non-EB (lead_form_type == 2), the Excel attachment comes from Google Sheets:
| recipient_type | Sheet used | Status after send |
|---|---|---|
internal |
+ QCR if status is qcr_created/qcr_sent, else RFQ |
+ Unchanged | +
insurer |
+ misc.rfq_sheet_id |
+ rfq_sent (unless already past that stage) |
+
client |
+ misc.qcr_sheet_id |
+ qcr_sent |
+
placement |
+ createAndDownloadPlacementSheet() — copies QCR sheet |
+ won + saves placement/payment/installment fields |
+
Implementation detail: downloadFileFromGoogleSheet() exports the chosen sheet to a temp
+.xlsx under writable/tmp/ before MailHelper::send_email().
Opened from list action → openPlacementFromList(leadId):
GET /rfq/placementData/{id} — pre-fills policy dates, premium, CD, installments, contacts.GET /leads/mailTemplate?template_type=placement — subject/body/attachments.POST /leads/sendMail with recipient_type=placement.won,
+ persists placement_date, premium_amount, cd_amount, installments, etc.| Status | Label | Set by |
|---|---|---|
queued | In-Queued | Create form (default) |
rfq_created | RFQ Created | createRfqSheet() |
rfq_sent | RFQ Sent | Insurer mail |
qcr_created | QCR Created | createQcrSheet() |
qcr_sent | QCR Sent | Client mail |
won | Won | Placement mail |
lost | Lost | User sets on edit form + lost reason |
{
+ "rfq_sheet_id": "…",
+ "qcr_sheet_id": "…",
+ "placement_sheet_id": "…"
+}
+
+Methods in LeadsController.php used by the list Non-EB flow:
| Method | Triggered from |
|---|---|
createLead() | Form submit (create + edit) |
getLeadNonEB() | Add / Edit navigation |
getPolicyTypeFields() | Policy type change on form |
createRfqSheet() | RFQ action |
createQcrSheet() | QCR action |
getLeadMailTemplate() | All mail modals |
getPlacementData() | Placement modal pre-fill |
uploadLeadAttachment() | Attachment upload in mail modals |
sendMailWithAttachement() | All mail sends + placement |
downloadFileFromGoogleSheet() | Mail attachment (internal/insurer/client) |
createAndDownloadPlacementSheet() | Placement mail attachment |
getLeadEmailHistory() | Email History modal |
buildRfqPlaceholderData() | RFQ sheet placeholder fill (helper) |
policy_type before first RFQ.policy_type.misc.rfq_template_sheet_id per product (see sheet ID per product).RFQ_PARENT_FOLDER_ID in .env and share folder/templates with the service account.Config\RfqConfig::$rfqPlaceholders.$isNonEb in leads_list.php.[1, 5, 2, 3] or BUSINESS_SUPPORT_TEAM_ID in user team.createQcrSheet fails if rfq_sheet_id is missing.queued.POST /leads/create; presence of hidden id triggers update.In short:
+check_si).| Area | Location |
|---|---|
| UI | +app/Views/employee_upload.php — action SI Enhancement |
+
| Upload | +EmployeeController::employeesUplodWithEvents |
+
| Validation | +excelFileFormatValidation, excelFileDataValidation |
+
| SI process | +employeesSIEnhanceProcess |
+
| Onboard SI path | +employeesSIEnhanceProcessWhileOnbboard — SI during dependent addition onboard (not Excel) |
+
| Job | +employeesSIEnhanceProcess in JobWorker.php |
+
| Helpers | +excel_util_helper.php — transform_si_excel_row_to_calculatable_format, premium_calculation_manager |
+
Routes (group /employee, authMVC):
GET /employee/upload — upload screenPOST /employee/upload — upload-action-type=si_enhancementGET /employee/excel_error/{file_id} — validation errorsfiles.policy_id is the client policy id. Slab rates load via
+ getPolicySlabRatesForEmpOnboard(policy_id, client_id).
+ | Step | < 1 MB | ≥ 1 MB |
|---|---|---|
| Format validation | Inline on upload | Job excelFileFormatValidation |
| Data validation | Job excelFileDataValidation | Same |
| SI enhance | Job employeesSIEnhanceProcess | Same |
$si_enhance_excel_columns — 5 columns (A–E).SI for mandatory rules.check_si against policy slabs (same helper as inception SI column).d-M-Y.| Code | Meaning |
|---|---|
| 1 | Mandatory missing |
| 2 | Wrong format |
| 3 | Not in allowed list |
| 4 | Custom validation failed (e.g. invalid Augmented SI for slab) |
| 5 | File / policy / slab problem |
| 6 | Column headers wrong |
name_and_empid_check_in_db for si_enhancement:
employee_polices on the policy.On success → queues employeesSIEnhanceProcess.
Per row:
+removeNumberFormatting).employee_polices row.transform_si_excel_row_to_calculatable_format (uses family max age/count).premium_calculation_manager → new premium for the augmented SI.employee_polices (actions = si):| field_name | new_value source |
|---|---|
basic_cover_si | Excel Augmented SI (column D) |
premium | Recalculated premium from rack logic |
si_enhancement_date | Excel Date of SI Enhancement (column E) |
+ Sets files.status = success when the loop completes. Skipped rows (duplicate pending SI, missing member)
+ are logged only — the file still succeeds.
+
| Col | Header | Required | Notes |
|---|---|---|---|
| A | S.No | Yes | |
| B | EMP ID | Yes | |
| C | NAME OF EMP/DEP | Yes | Must match DB |
| D | Augmented SI | Yes | New SI; validated via check_si |
| E | Date of SI Enhancement | Yes | d-M-Y |
Enhance Self SI — one row:
+ +| EMP ID | NAME | Augmented SI | Date of SI Enhancement |
|---|---|---|---|
| EMP001 | Raj Kumar | 1000000 | 19-May-2026 |
Enhance spouse only — separate row (premium uses that member’s relationship for rack selection):
+ +| EMP ID | NAME | Augmented SI | Date of SI Enhancement |
|---|---|---|---|
| EMP001 | Priya Kumar | 500000 | 19-May-2026 |
upload-action-type=si_enhancement; note file_id./employee/excel_error/{file_id} — codes 4 (SI/slab) or 10 (member missing).emp_endorsement where file_id = upload id, actions = 'si', status = 'pending' — expect up to 3 rows per member (same group_key).new_value on basic_cover_si and premium to Excel and rack expectations.check_si in step 1 must pass before the job runs.files.status = success does not guarantee every row created endorsements.employee_polices.basic_cover_si changes after endorsement processing.+ Related: + Inception, + Correction, + EB rack rate calculation. +
diff --git a/app/Views/leads_form.php b/app/Views/leads_form.php index 36d930c8..d6942deb 100644 --- a/app/Views/leads_form.php +++ b/app/Views/leads_form.php @@ -163,7 +163,8 @@ -->