From f336342c86fe31d6e88be9acbd0a61ce888c10f5 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Mon, 18 May 2026 18:00:01 +0530 Subject: [PATCH 1/7] GWM : Manual merge option --- app/Helpers/merge_pdf_helper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Helpers/merge_pdf_helper.php b/app/Helpers/merge_pdf_helper.php index c222289c..e1ccc426 100644 --- a/app/Helpers/merge_pdf_helper.php +++ b/app/Helpers/merge_pdf_helper.php @@ -189,7 +189,7 @@ if (! function_exists('merge_ticket_pdfs')) { $insertData = [ 'ticket_id' => $ticket_master_id, - 'ticket_type' => $opts['ticket_type'], + // 'ticket_type' => $opts['ticket_type'], 'file_type' => MERGED_CLAIM_FILE_TYPE, 'doc_name' => 'MERGED_CLAIM_DOCS_PDF', 'file_name' => $mergedName, From 55486f26650219adc15412ffef4ac871270dd0c1 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Tue, 19 May 2026 10:53:20 +0530 Subject: [PATCH 2/7] GWM : session issue --- app/Controllers/LoginController.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/Controllers/LoginController.php b/app/Controllers/LoginController.php index fb073f21..209d797c 100755 --- a/app/Controllers/LoginController.php +++ b/app/Controllers/LoginController.php @@ -20,12 +20,9 @@ class LoginController extends BaseController public function index() { - // $session_data = ['isLoggedIn' => True ,'userid' => '25']; - // set_session_data($session_data); - // $isLoggedIn = check_session(); $isLoggedIn = check_session(); $hasCookie = check_cookie(); - if ($isLoggedIn && $hasCookie) { + if ($isLoggedIn || $hasCookie) { return redirect()->to(base_url('/dashboard/view')); } return view('login'); From eb3d58c69a12286c52cb2f6a5ec735441c63e2d5 Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Tue, 19 May 2026 17:50:04 +0530 Subject: [PATCH 3/7] FIX_LIVE_ISSUE --- app/Controllers/ApiServiceController.php | 53 ++++--- app/Controllers/TicketServiceController.php | 1 + app/Models/ClaimFilesModel.php | 14 ++ app/Models/TicketMasterModel.php | 19 ++- app/Views/leads_form.php | 57 ++++++- app/Views/ticket_edit_onbording.php | 19 ++- app/Views/ticket_form_gmc.php | 145 ++++++++++++++---- .../assets/js/pages/leads_form_validation.js | 124 ++++++++++++++- 8 files changed, 371 insertions(+), 61 deletions(-) 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/TicketServiceController.php b/app/Controllers/TicketServiceController.php index 24e5e463..ffa6f154 100644 --- a/app/Controllers/TicketServiceController.php +++ b/app/Controllers/TicketServiceController.php @@ -1297,6 +1297,7 @@ class TicketServiceController extends AdminController } else { //check the employee and insured + $params['is_from'] = 'claim_dump'; $employee_data = $ticketMasterModal->getEmployeeAndEmployeePolicyDetails($params) ?? []; // dd($employee_data); if (count($employee_data) == 0) { diff --git a/app/Models/ClaimFilesModel.php b/app/Models/ClaimFilesModel.php index 055b9b3b..66bd25ed 100644 --- a/app/Models/ClaimFilesModel.php +++ b/app/Models/ClaimFilesModel.php @@ -59,4 +59,18 @@ class ClaimFilesModel extends Model return $data; } + + /** + * Whether the claim (ticket) has at least one active PDF in claim_files. + */ + public function hasPdfFileForTicket($ticketId): bool + { + return $this->where('ticket_id', $ticketId) + ->where('is_active', 1) + ->groupStart() + ->where('mime_type', 'pdf') + ->orWhere('mime_type', 'application/pdf') + ->groupEnd() + ->countAllResults(false) > 0; + } } diff --git a/app/Models/TicketMasterModel.php b/app/Models/TicketMasterModel.php index 8a6128ca..f6ea661d 100644 --- a/app/Models/TicketMasterModel.php +++ b/app/Models/TicketMasterModel.php @@ -1118,12 +1118,23 @@ class TicketMasterModel extends Model $policy_no = $params['policy_no'] ?? null; $relationship = $params['relationship'] ?? null; $insured_name = $params['insured_name'] ?? null; + $is_from = $params['is_from'] ?? null; // $client_name = "6-Eleven"; // $emp_code = "EMP0020K12"; // $emp_name = "Lokesh"; // $policy_no = "GMC-1999/2000/2021/2022"; + $base_policy_id = null; + if($is_from == "claim_dump"){ + $client_policy_data = $this->db->table('client_policy')->where('policy_no', trim($policy_no))->where('is_active', 1)->get()->getRowArray(); + if(isset($client_policy_data) && !empty($client_policy_data)){ + if(!empty($client_policy_data['base_policy'])) { + $base_policy_id = $client_policy_data['base_policy']; + } + } + } + $builder = $this->db->table('employees e'); $builder->select(" e.id as emp_id, @@ -1161,7 +1172,13 @@ class TicketMasterModel extends Model $builder->where('e.emp_code', trim($emp_code)); $builder->where('e.name', trim($emp_name)); $builder->where('c.client_name', trim($client_name)); - $builder->where('cp.policy_no', trim($policy_no)); + + if(!empty($base_policy_id)) { + $builder->where('cp.id', trim($base_policy_id)); + } else { + $builder->where('cp.policy_no', trim($policy_no)); + } + $builder->where('LOWER(e.relationship)', strtolower('self')); $query = $builder->get(); diff --git a/app/Views/leads_form.php b/app/Views/leads_form.php index 36d930c8..b352748f 100644 --- a/app/Views/leads_form.php +++ b/app/Views/leads_form.php @@ -163,7 +163,8 @@ -->
+ enctype="multipart/form-data" + data-parsley-excluded="input[type=button], input[type=submit], input[type=reset], input[type=hidden], [disabled], :hidden, .select2-search__field"> @@ -487,7 +488,7 @@ function restoreActualLeadGstNumber() { if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0 && actualLeadGstNumber) { - $('#gst').val(actualLeadGstNumber); + $('#gst').val(String(actualLeadGstNumber).trim().toUpperCase()); } } @@ -1774,15 +1775,51 @@ event.preventDefault(); + if (typeof window.prepareLeadsFormForSubmit === 'function') { + window.prepareLeadsFormForSubmit(); + } + var isValid = $('#leads_form_id').parsley().validate(); if (!isValid) { - $('#leads_form_id').find('input, select, textarea').each(function() { - if ($(this).parsley().isValid() === false && !$(this).val()) { - console.log('Empty field ID:', this.id); + var invalidFields = typeof window.getLeadsFormInvalidFields === 'function' + ? window.getLeadsFormInvalidFields() + : []; + + if (invalidFields.length) { + console.table(invalidFields); + invalidFields.forEach(function(f) { + console.log('Invalid field:', f.label || f.name || f.id, f); + }); + + var firstInvalid = invalidFields[0]; + var fieldLabel = firstInvalid.label || firstInvalid.name || firstInvalid.id || 'A required field'; + var fieldMessage = (firstInvalid.messages && firstInvalid.messages.length) + ? firstInvalid.messages[0] + : (firstInvalid.isEmpty ? 'This field is required.' : 'Please check this field.'); + + if (typeof toastr !== 'undefined') { + toastr.warning(fieldLabel + ': ' + fieldMessage, 'Validation'); } - }); - console.log('Form is Empty', 'Warning'); + + if (firstInvalid.id) { + var $invalidField = $('#' + firstInvalid.id); + if ($invalidField.length) { + var scrollTarget = $invalidField.closest('.form-group, .card, .dynamic-form-row'); + if (scrollTarget.length) { + $('html, body').animate({ + scrollTop: scrollTarget.offset().top - 100 + }, 300); + } + $invalidField.focus(); + } + } + } else { + console.warn('Form validation failed'); + if (typeof toastr !== 'undefined') { + toastr.warning('Please correct the highlighted fields.', 'Validation'); + } + } return; } @@ -2564,6 +2601,10 @@ toggleRequiredFields(); restoreActualLeadGstNumber(); restoreActualLeadContactDetails(); + + if (typeof window.refreshLeadsFormValidation === 'function') { + window.refreshLeadsFormValidation(); + } } function toggleRequiredFields() { @@ -2694,7 +2735,7 @@ actualLeadClientName = actual_lead_client_details.company_name || ''; $('#client_name').val(actualLeadClientName); - actualLeadGstNumber = actual_lead_client_details.gst_number || actual_lead_client_details.gst || ''; + actualLeadGstNumber = (actual_lead_client_details.gst_number || actual_lead_client_details.gst || '').toString().trim().toUpperCase(); $('#gst').val(actualLeadGstNumber); if (actual_lead_client_details.client_type !== undefined && actual_lead_client_details.client_type !== null && actual_lead_client_details.client_type !== '') { $('#client_type').val(String(actual_lead_client_details.client_type)); diff --git a/app/Views/ticket_edit_onbording.php b/app/Views/ticket_edit_onbording.php index 857a2796..d3fe5dea 100644 --- a/app/Views/ticket_edit_onbording.php +++ b/app/Views/ticket_edit_onbording.php @@ -39,12 +39,25 @@
+ Manual TPA Claim Push'; + } + if (!empty($ticket_data['tpa_claim_push_reference_no'])) { + $tpaActionButtons .= ' Fetch Claim Status'; + } + ?> - + B["Save JSON on disk"] + B --> C["Edit rules in UI"] + D["Policy data POST"] --> E["Load matching JSON"] + E --> F["Match rule and return payout"] +
+
+ +

In short:

+
    +
  • Setup: RuleImportController + commission_file_upload.php — import and manage rules.
  • +
  • Runtime: POST getCommissionInsuranceCommissionController::initiateCommissionCalc — read JSON, pick first matching rule, return payout.
  • +
  • Rules file path: writable/uploads/commission/rules/{MONYYYY}/{insurer_id}_{department}.json (e.g. SEP2025/5_motor.json).
  • +
+ +

Key files

+ + + + + + + + + + + + + +
PartFile
Upload list UIapp/Views/commission_file_upload.php
Rules editor UIapp/Views/commission_rules_list.php
Upload and editor APIapp/Controllers/RuleImportController.php
Excel parsingruleImportService (via Config\Services::ruleImportService())
Payout calculation APIapp/Controllers/InsuranceCommissionController.php
Upload metadata DBcommission_files (CommissionFilesModel)
+ +

Routes

+ +

Admin (commission group):

+ + + + + + + + + + + + + + + + +
RouteHandler
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
+ +

Runtime API:

+
POST getCommission
+  → InsuranceCommissionController::initiateCommissionCalc
+  → filter: CommissionApiFilter
+ +

How to build the commission file

+ +

+ Use the template from the upload screen (Download sample file) or copy from + 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). +

+ +
+ ! +
+ Use the current column layout + Some older CSVs in 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)

+ +

Header text must match exactly (one row per rule, starting row 2). Empty cells are allowed and simply skip that condition.

+ + + + + + + + + + + + + + + + + + + + + + + +
ColumnPurposeExample
S.NoSerial (not used in logic)1
Premium TypeMaps to policy_typeOD, TP, COM
Vehicle Typevehicle_type; comma = multiple (IN)Two Wheeler, Four Wheeler
Vehicle Sub Typevehicle_sub_typeCar, PCV
Make / ModelVehicle make and modelHonda, i10
CC Min / CC MaxCubic capacity range1000, 3000
Fuel TypeComma-separated fuelsPetrol, Diesel
Vehicle Age Min / MaxVehicle age range (years)1, 5
Vehicle Weight Min / MaxWeight range (kg)2000, 3000
RTO State / RTO Codegeo_rto_state, geo_rto_cityRJ, 41
Renewal Typerenewal_typeOnline, Cash, Card
Commission Typepercentage, composite, flat, tieredpercentage
Commission Value% or flat amount (required for percentage / flat)15 or 500
Commission Params (TP)Composite: % on TP premium18
Commission Params (OD)Composite: % on OD premium10
Commission Params (PA)Composite: % on PA premium0 or empty
+ +

Commission types (what to fill)

+ + + + + + + + + + + + + + + + + + + + + + +
TypeFill in sheetBecomes 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 premium
+ +

Sample rows (from project files)

+ +

Template header + rows (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,
+ +

Example A — composite two-wheeler (from writable/.../rules/NOV2025/1_motor.json):

+
    +
  • Vehicle Type = Two Wheeler, CC Min/Max = 100
  • +
  • Commission Type = composite, TP = 10, OD = 25
  • +
  • Result: 10% on TP premium + 25% on OD premium when policy matches
  • +
+ +

Example B — percentage four-wheeler:

+
    +
  • Vehicle Type = Four Wheeler, CC = 1000, Vehicle Age Min/Max = 5
  • +
  • Commission Type = percentage, Commission Value = 10
  • +
  • Result: 10% of total premium
  • +
+ +

Example C — composite TP-only:

+
    +
  • Premium Type = TP, Vehicle Type = Four Wheeler
  • +
  • Commission Type = composite, Commission Params (TP) = 10
  • +
  • Result: 10% on TP premium only
  • +
+ +

Example D — flat amount:

+
    +
  • Vehicle Type = Two Wheeler,Four Wheeler (comma → matches either)
  • +
  • Commission Type = flat, Commission Value = 500
  • +
  • Result: fixed ₹500 when matched
  • +
+ +

Tips:

+
    +
  • Min must not be greater than Max (CC, age, weight) or the row fails validation.
  • +
  • On failure, download the annotated file — errors are written into the sheet.
  • +
  • Supported formats: .csv, .xlsx, .xls, .ods.
  • +
  • Upload UI departments: motor, health; import logic is built for motor columns today.
  • +
+ +

Rule upload

+ +

From commission_file_upload.php the user picks insurer, commission month, department (motor / health), and an Excel/CSV file.

+ +
    +
  1. checkSameEntry — if a successful upload already exists for the same trio, SweetAlert offers Overwrite or Append (overwrite=1 or 0 on POST).
  2. +
  3. upload — stores file under writable/uploads/commission/files/, inserts commission_files row (pending).
  4. +
  5. ruleImportService->processUpload() — validates Excel rows; on success returns rules array; on failure returns annotated_file for download.
  6. +
  7. On success — writes JSON to rules/{MONYYYY}/{insurer_id}_{department}.json; sets file_status=success and rules_count.
  8. +
  9. On failure — file_status=failed; user downloads annotated_{filename} via downloadErrorFile.
  10. +
+ +

Rules editor

+ +

+ For successful uploads, action View Rules opens + commission/rules/list/{file_id} (commission_rules_list.php). +

+
    +
  • Lists rules from the JSON file for that upload’s insurer, month, and department.
  • +
  • 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.
  • +
  • Deleting the whole upload marks all rules with that file_id as deleted in JSON, then sets commission_files.is_active=0.
  • +
+ +

Rule JSON shape

+ +

Each rule is roughly:

+
{
+  "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"
+  }
+}
+ +

Calculation types in 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_id
  • +
  • department — motor, health, etc.
  • +
  • Fields referenced in rule conditions and calculation bases (e.g. premium, od_premium)
  • +
+ +
+
+flowchart TD + A["POST getCommission"] --> B{"Required fields present?"} + B -->|No| C["Validation error"] + B -->|Yes| D["Load rules JSON for month, insurer, department"] + D --> E{"File exists?"} + E -->|No| F["Rules file not found"] + E -->|Yes| G["Find first rule where all conditions match"] + G --> H{"Rule found?"} + H -->|No| I["No matching rules"] + H -->|Yes| J["Apply calculation type"] + J --> K["Return payout and rule"] +
+
+ +

Rules with is_deleted: false are loaded; the first matching rule wins (no priority field yet).

+ +

Developer steps

+ +
    +
  1. Open /commission/list (logged-in admin).
  2. +
  3. Download sample file, fill rules for insurer + month + department, upload.
  4. +
  5. If validation fails, download the annotated error file and fix the sheet.
  6. +
  7. Use View Rules to tweak conditions or calculation without re-uploading the whole file.
  8. +
  9. Test payout: POST getCommission with the same insurer, department, and a policy_issue_date in that commission month.
  10. +
+ +

Common pitfalls

+ +
    +
  • Month folder must match policy date — upload uses commission month; API uses policy_issue_date to resolve the same MONYYYY folder.
  • +
  • Append vs overwrite — append merges JSON arrays; overwrite backs up the old file then replaces.
  • +
  • Departments — upload UI currently offers motor and health; API department string must match the JSON filename slug.
  • +
  • HTTP 200 on upload errors — check status and code in the JSON body, not only HTTP status.
  • +
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 @@ + + +

+ BDS Insurer statement lets finance users upload an insurer-provided Excel statement, + validate each row against NHance policy transactions (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. +

+ +
+ i +
+ Auth + Statement routes use the authMVC filter. Upload and list are browser AJAX/form calls from an authenticated session, not public API endpoints. +
+
+ +

Overview

+ +
+
+flowchart LR + A[Upload Excel] --> B[validateInsurerStatement] + B --> C{All rows OK?} + C -->|Yes| D[updateInsurerStatement] + C -->|No| E[file_status failed] + D --> F[co_share_stmt_details] +
+
+ +

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). +

+ +
+
+flowchart TD + A["Read Excel"] --> B["Fetch NHance rows for policies in the file"] + B --> C["Check each non-empty row"] + C --> D{"Policy and endorsement found in NHance?"} + D -->|"Yes, first time in file"| E["Row valid"] + D -->|"Same combo again"| F["Duplicate"] + D -->|"Policy not found"| G["Invalid policy"] + D -->|"Policy OK, wrong endorsement"| H["Invalid endorsement"] + E --> I{"Any bad rows?"} + F --> I + G --> I + H --> I + I -->|No| J["Validation passes"] + I -->|Yes| K["Validation fails"] +
+
+ +

In short:

+
    +
  • Empty rows are ignored.
  • +
  • A row is valid only if that policy + endorsement exists in NHance and appears once in the upload.
  • +
  • If anything fails, the whole file is marked failed and errors are shown per row in the UI.
  • +
+ +

Key files and routes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AreaLocation
UIapp/Views/insurer_statement_list.php — DataTable list, upload modal, validation error modal, invoice modal
Controllerapp/Controllers/PolicyTransactionController.php
Statement header modelapp/Models/InsurerStatements.phpinsurer_statements
Line-item modelapp/Models/COShareStmtDetailsModel.phpco_share_stmt_details
NHance source rowsapp/Models/PTCOShareDetailsModel.phpgetNonReconcileredPolicyTransactionByPolicyAndEndorsement()
Sample Excelpublic/sample_excel/insurer_stament_sample.xlsx
+ +

Routes (prefix policy_tranction/statement, filter authMVC):

+ + + + + + + + + + + + + + + + +
MethodRouteHandler
GETliststatementList
POSTuploaduploadInsurerStatement
GETdownloadSampleInsurerStatementSample file download
GETdownloadInsurerStatement/(:num)Uploaded file download
GETgetFileErr/(:any)Validation failure JSON for modal
GETgetInsurerStatementMonthUsed to disable already-used statement numbers
GETdeleteStatement/(:any)Soft-delete statement + related rows
GETgetPaymentDetails/(:any)Invoice modal data
POSTsaveInvoicePaymentDetailsInvoice / 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: +

+ +
    +
  • Insurer branch, statement month, statement serial no, filename (download link), line items count
  • +
  • file_statussuccess or failed (failed rows show an alert icon → error modal)
  • +
  • invoice_status — pending / generated / sent / payment received
  • +
  • Actions (non-failed only): invoice status update, delete
  • +
+ +

+ Upload form fields (#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()

+ +
    +
  1. Validates uploaded file: Excel MIME types, max 16 MB (max_size[statement,16384] KB).
  2. +
  3. Moves file to WRITEPATH . 'uploads/statements/' (see createStatementFolder() for folder creation).
  4. +
  5. Parses POST: insurer as {insurer_id}-{branch_id}, statement_month (converted to first-of-month Y-m-d), statement_nostmt_sno.
  6. +
  7. Inserts insurer_statements row via InsurerStatements model.
  8. +
  9. Calls validateInsurerStatement(['file_id' => $file_id]).
  10. +
  11. If validation passes, calls updateInsurerStatement(['file_id' => $file_id]).
  12. +
  13. On validation failure: responds with dataStatus: false, error_data, error_code (HTTP 200).
  14. +
  15. On success: sets invoice_status = 'pending' and returns dataStatus: true.
  16. +
+ +

validateInsurerStatement($params)

+ +

Runs immediately after upload (and can be re-run manually in dev with a hard-coded file_id in statementList() comment).

+ +

Steps

+ +
    +
  1. Load insurer_statements by file_id; fail if missing or physical file absent under writable/uploads/statements/.
  2. +
  3. Load active sheet via PhpSpreadsheet; drop header row; sanitize with ExcelSanitizeHelper::sanitizeArrayData().
  4. +
  5. Collect unique policy numbers from column B (index 1), skipping empty rows via check_row_is_empty_or_null().
  6. +
  7. Fetch NHance candidates: + PTCOShareDetailsModel::getNonReconcileredPolicyTransactionByPolicyAndEndorsement(insurer_id, branch_id, policy_no[]) + — matches pt.policy_no for completed transactions on that insurer branch.
  8. +
  9. Build lookup keys policy_no|endorsement_no (sanitized) for source and Excel rows.
  10. +
  11. For each non-empty Excel row, match policy + endorsement; track duplicates in $matched_entry.
  12. +
  13. On mismatch, append row-wise HTML messages under error_data[row_index].
  14. +
  15. Update insurer_statements.line_items, file_status (success / failed), reason (JSON).
  16. +
+ +

sanitizeStatementLookupValue()

+ +

Private helper trims Unicode spaces and strips zero-width / BOM characters from policy and endorsement values before comparison — avoids “looks equal” mismatches in Excel.

+ +

Validation error codes

+ + + + + + + + + + +
error_codeMeaningUI
0DB row missing or physical file not foundPlain 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
+ +

Row keys in 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)

+ +

Runs only when validation returned status: true.

+ +
    +
  1. Same file load / sanitize path as validation.
  2. +
  3. Rebuild policy list and source lookup (policy + endorsement → array of pt_co_share_details rows).
  4. +
  5. For each Excel row with a matching source entry, compute brokerage totals and variance: +
      +
    • BP: amount col 3, brokerage col 5
    • +
    • TP: amount col 4, brokerage col 6
    • +
    • TEP: forced to 0 in current code
    • +
    • reward from col 7
    • +
    • variance = exp_amt - total_amt (from source exp_amt)
    • +
    +
  6. +
  7. COShareStmtDetailsModel::insertBatch($data_to_update) — one row per matched Excel line.
  8. +
  9. Sets file_status = success, invoice_status = pending, clears/sets reason.
  10. +
+ +

Excel column mapping (0-based index)

+ +

Header row is removed; data columns used by validation/update:

+ + + + + + + + + + + + + + +
IndexColumnUse
1BPolicy number (required for matching)
2CEndorsement number
3DActual BP amount
4EActual TP amount
5FActual BP brokerage
6GActual TP brokerage
7HReward
+ +

Download the canonical layout from the list page link → downloadSampleInsurerStatement.

+ +

NHance source query

+ +

getNonReconcileredPolicyTransactionByPolicyAndEndorsement() joins:

+ +
    +
  • pt_co_share_details (active) → policy_transaction (active, status = completed)
  • +
  • Filtered by insurer_id, insurer_branch_id, and pt.policy_no IN (...)
  • +
+ +

+ Matching is on sanitized 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

+ +

After a successful upload, users manage invoice lifecycle from the list (separate from upload/validate):

+ +
    +
  • invoice_status: pending, generated, sent, payment_received
  • +
  • saveInvoicePaymentDetails — JSON POST from invoice modal
  • +
  • deleteStatement($id) — soft-deletes co_share_stmt_details, inv_payment_details, and insurer_statements for that id
  • +
+ +

+ getInsurerStatementMonth returns successful statements for insurer+month so the UI can + disable statement numbers already used (disableStatementNo() in the view). +

+ +

Developer steps

+ +
    +
  1. Ensure writable/uploads/statements/ exists and is writable (or call createStatementFolder() once).
  2. +
  3. Open policy_tranction/statement/list in a logged-in session.
  4. +
  5. Use sample Excel; pick insurer branch and month; choose an unused statement number (1–7 per insurer/month).
  6. +
  7. Confirm policy/endorsement exist on a completed BDS transaction for that insurer branch.
  8. +
  9. On failure, open the alert icon → modal calls getFileErr/{id} and renders reason JSON.
  10. +
  11. To debug validation only: temporarily uncomment the validateInsurerStatement / updateInsurerStatement one-liner in statementList() with a known file_id.
  12. +
+ +

Common pitfalls

+ +
    +
  • Hidden Excel characters — policy/endorsement must pass sanitizeStatementLookupValue(); re-type values if NHance shows a match but upload fails.
  • +
  • Duplicate policy + endorsement in the same file → error_code 2, duplicate message on second row.
  • +
  • Validation failed but file on diskinsurer_statements row remains; user sees failed status; re-upload needs a new statement or delete the failed row.
  • +
  • Statement number reuse — only successful uploads for that insurer/month block numbers in the dropdown via getInsurerStatementMonth.
  • +
  • Upload response HTTP 200 on error — front-end checks dataStatus, not status code alone.
  • +
  • Legacy handlersvalidateInsurerStatementOld, updateInsurerStatementOLD remain in the controller; production path is the non-Old methods documented here.
  • +
+ + + +
    +
  • BDS reports: policy_tranction/report/list, variance, finance, outstanding lists
  • +
  • Daily BDS cron mail: cronDailyBDSReport (separate from statement upload)
  • +
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 @@ + + +

+ Correction updates existing member data on an active policy via Excel 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. +

+ +

+ Processing is in EmployeeServiceController::employeesCorrectionProcess, queued after the same + format and data validation steps used for inception and deletion. +

+ +

Overview

+ +
+
+flowchart LR + A["Upload Excel action correction"] --> B["excelFileFormatValidation"] + B --> C["excelFileDataValidation"] + C --> D["employeesCorrectionProcess job"] + D --> E["emp_endorsement pending on employees"] + B -->|errors| F["files.status failed"] + C -->|errors| F + D -->|loop done| G["files.status success"] +
+
+ +

In short:

+
    +
  • Step 1 — Format: 8 columns (A–H); field must be one of four allowed names; dates d-M-Y.
  • +
  • Step 2 — Data: Member must exist (emp code + name + active policy); code 10 if not found.
  • +
  • Step 3 — Correction: One pending endorsement per row per field (skips duplicate pending corrections).
  • +
  • Each Excel row = one field change for one member (not a full-family operation).
  • +
+ +

Key files and routes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AreaLocation
UIapp/Views/employee_upload.php — action Correction
UploadEmployeeController::employeesUplodWithEvents
ValidationexcelFileFormatValidation, excelFileDataValidation
Correction processEmployeeServiceController::employeesCorrectionProcess
Column config APIgetCorrectionExcelColumns() — used when building correction Excel programmatically
JobemployeesCorrectionProcess in JobWorker.php
EndorsementsEmpEndorsementModelactions = c, table_name = employees, status = pending
+ +

Routes (group /employee, authMVC):

+
    +
  • GET /employee/upload — upload screen
  • +
  • POST /employee/uploadupload-action-type=correction
  • +
  • GET /employee/excel_error/{file_id} — validation errors
  • +
+ +
+ i +
+ files.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 validationInline on uploadJob excelFileFormatValidation
Data validationJob excelFileDataValidationSame
Correction processJob employeesCorrectionProcessSame
+ +

Step 1: excelFileFormatValidation

+ +
    +
  • Uses $correction_excel_columns8 columns (A–H).
  • +
  • Action code C for mandatory rules on correction-specific columns.
  • +
  • Field (column D): only name, dob, relationship, email_corporate.
  • +
  • Date of Correction (F): d-M-Y.
  • +
  • Change event (G) and Value (E) are mandatory.
  • +
+ +

Format error codes

+ + + + + + + + + + + + + +
CodeMeaning
1Mandatory missing
2Wrong format
3Not in allowed list (e.g. invalid Field value)
4Custom validation failed
5File / policy problem
6Column headers wrong
+ +

Step 2: excelFileDataValidation

+ +

Rows grouped by EMP ID; for correction the critical check is name_and_empid_check_in_db:

+
    +
  • Each row must match an active employee on the uploaded policy (emp code + name).
  • +
  • Not found → error code 10 (“Record Not found”).
  • +
+ +

On success → queues employeesCorrectionProcess (with optional batch_file_id for TPA multi-file flows).

+ +

Step 3: employeesCorrectionProcess

+ +
+
+flowchart TD + A["Read each Excel row"] --> B["Match emp_code plus name on active policy"] + B --> C{"Pending correction for same field?"} + C -->|Yes| D["Skip row"] + C -->|No| E["Insert emp_endorsement actions c"] + E --> F["old_value from DB new_value from Excel"] +
+
+ +

Per row:

+
    +
  • 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-d
  • +
  • date_of_correction ← column F (converted to Y-m-d)
  • +
  • remarks ← column H (optional)
  • +
  • old_value ← current value from employees.{field_name}
  • +
+ +

+ Skips insert when a pending correction endorsement already exists for the same + emp_code, name, and field_name + (actions = c, endorsement_id IS NULL, status != truncated). +

+ +

+ Sets 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). +

+ +

+ TPA batch: If batch_file_id is set, also queues + updateEmployeeDataFromTpa, reconTpaApiDataWithEmployeepolicies, and + initializeDeletionProcessForTpaApiData. +

+ +

Correction Excel columns

+ + + + + + + + + + + + + + + +
ColHeaderRequiredNotes
AS.NoYes
BEMP IDYes
CNAME OF EMP/DEPYesMust match DB before correction
DFieldYesname, dob, relationship, email_corporate
EValueYesNew value; DOB as d-M-Y
FDate of CorrectionYesd-M-Y
GChange eventYese.g. correction
HRemarksNoStored on endorsement
+ +

Row layout examples

+ +

Fix DOB — one row, one field:

+ + + + + + + + + + + + + + + + +
EMP IDNAMEFieldValueDate of CorrectionChange eventRemarks
EMP001Raj Kumardob15-Jan-198519-May-2026correctionTypo in upload
+ +

Multiple fixes — use separate rows (same or different members):

+ + + + + + + + + +
EMP IDNAMEFieldValue
EMP001Raj Kumaremail_corporateraj.kumar@company.com
EMP001Priya KumarrelationshipSpouse
+ +

Developer steps

+ +
    +
  1. Upload with upload-action-type=correction; note file_id.
  2. +
  3. On validation failure, check /employee/excel_error/{file_id} for code 10.
  4. +
  5. After success, query emp_endorsement where file_id = upload id, actions = 'c', status = 'pending'.
  6. +
  7. Compare field_name, old_value, new_value per row to the Excel.
  8. +
  9. To generate correction Excel in code, use getCorrectionExcelColumns() for header layout.
  10. +
+ +

Common pitfalls

+ +
    +
  • Name must match DB — correction identifies the member by current emp_code + name; rename via a name field row uses the old name in column C.
  • +
  • Only four fields — mobile, SI, band, etc. are not supported in this upload path.
  • +
  • Duplicate pending correction — second upload for the same field is skipped until the first endorsement is processed or truncated.
  • +
  • File success vs rowsfiles.status = success does not mean every row created an endorsement.
  • +
  • Not live updateemployees columns change only after endorsement approval/application.
  • +
+ +

+ Related: + Inception, + Deletion. +

diff --git a/app/Views/docs/deletion.php b/app/Views/docs/deletion.php new file mode 100644 index 00000000..aff0fdba --- /dev/null +++ b/app/Views/docs/deletion.php @@ -0,0 +1,259 @@ + + +

+ Deletion removes active members from a client policy via Excel upload + (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. +

+ +

+ Core processing lives in EmployeeServiceController::employeeDisembark. + EmployeeController::initializeDeletionProcessForTpaApiData is a separate TPA-reconcile path that + builds a deletion Excel file and calls the same disembark function. +

+ +

Overview

+ +
+
+flowchart LR + A["Upload Excel action deletion"] --> B["excelFileFormatValidation"] + B --> C["excelFileDataValidation"] + C --> D["employeeDisembark job"] + D --> E["emp_endorsement pending rows"] + B -->|errors| F["files.status failed"] + C -->|errors| F + D -->|always| G["files.status success"] +
+
+ +

In short:

+
    +
  • Step 1 — Format: 7 columns (A–G), mandatory exit fields per action code D.
  • +
  • Step 2 — Data: Member must exist in DB (emp code + name + active policy); code 10 if not found.
  • +
  • Step 3 — Disembark: Writes pending deletion endorsements; Self row removes whole family, dependent row removes one member.
  • +
  • Files < 1 MB run format validation inline; data validation and disembark are always queued.
  • +
+ +

Key files and routes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AreaLocation
UIapp/Views/employee_upload.php — action Deletion
UploadEmployeeController::employeesUplodWithEvents
ValidationEmployeeServiceController::excelFileFormatValidation, excelFileDataValidation
Deletion processEmployeeServiceController::employeeDisembark
TPA auto-deletionEmployeeController::initializeDeletionProcessForTpaApiData
JobemployeeDisembark in JobWorker.php
EndorsementsEmpEndorsementModelemp_endorsement (actions = d, status = pending)
+ +

Routes (group /employee, authMVC):

+
    +
  • GET /employee/upload — upload screen
  • +
  • POST /employee/uploadupload-action-type=deletion
  • +
  • GET /employee/excel_error/{file_id} — read files.reason after validation failure
  • +
+ +
+ i +
+ files.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 validationInline on uploadJob excelFileFormatValidation
Data validationJob excelFileDataValidationSame
DisembarkJob employeeDisembarkSame
+ +

Step 1: excelFileFormatValidation

+ +
    +
  • Uses $deletion_excel_columns7 columns (A–G).
  • +
  • Action code D drives mandatory fields: Change event, Date of exit, Reason for exit, Claim status.
  • +
  • Date of exit format: d-M-Y.
  • +
  • Claim status allowed: 0 or 1.
  • +
+ +

Format error codes

+ + + + + + + + + + + + + +
CodeMeaning
1Mandatory missing
2Wrong format
3Not in allowed list
4Custom validation failed
5File / policy problem
6Column headers wrong
+ +

Step 2: excelFileDataValidation

+ +

Rows are grouped by EMP ID. For deletion, the main check is name_and_empid_check_in_db:

+
    +
  • Each row must match an active employee + active employee_polices row on the uploaded policy/branch.
  • +
  • If not found → error code 10 (“Record Not found”) on that Excel row.
  • +
+ +

On success → queues employeeDisembark (not employeesOnboardPreprocess).

+ +

Step 3: employeeDisembark

+ +

Loads the Excel again and processes each non-empty row.

+ +
+
+flowchart TD + A["Match emp_code plus name on active policy"] --> B{"Found?"} + B -->|No| C["Log error skip row"] + B -->|Yes| D{"Pending deletion endorsement?"} + D -->|Yes| E["Log skip row"] + D -->|No| F{"Relationship Self?"} + F -->|No| G["Endorse this member only"] + F -->|Yes| H["Endorse all active family members"] + G --> I["emp_endorsement pending"] + H --> I +
+
+ +

Per matched member, four pending endorsement rows are inserted on employee_polices:

+
    +
  • date_of_exit ← Excel column E (converted to Y-m-d)
  • +
  • reason_for_exit ← column F
  • +
  • statusinactive
  • +
  • claim_status ← column G
  • +
+ +

+ Returns an array of processed employee.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):

+
    +
  1. Reads TPA reconcile file context (tpa_api_data, action_flag_status = D).
  2. +
  3. Builds candidate members and writes writable/uploads/excel/tpa_auto_deletion_{fileId}_{timestamp}.xls.
  4. +
  5. Inserts a new files row with action = deletion.
  6. +
  7. Calls employeeDisembark(['file_id' => $newFileId]) synchronously.
  8. +
  9. Exports endorsement data for TPA via getDeletionEmployeeDataForExportExcel.
  10. +
+ +

Deletion Excel columns

+ + + + + + + + + + + + + + +
ColHeaderRequiredNotes
AS.NoYes
BEMP IDYesEmployee / family code
CNAME OF EMP/DEPYesMust match DB name exactly
DChange eventYese.g. deletion
EDate of exitYesd-M-Y
FReason for exitYes
GClaim statusYes0 or 1
+ +

Row layout examples

+ +

Delete one dependent — only that name appears; Self row is not required in the file.

+ + + + + + + + +
RowEMP IDNAMEChange eventDate of exitReasonClaim
2EMP001Arjun Kumardeletion19-May-2026Resigned0
+

Result: endorsements for Arjun only (relationship ≠ Self).

+ +

Delete entire family — list the Self row; disembark loads all active family members for that EMP ID.

+ + + + + + + + +
RowEMP IDNAMEChange eventDate of exitReasonClaim
2EMP001Raj Kumardeletion19-May-2026Resigned0
+

Result: pending endorsements for Self + all active dependents on that policy (same exit date/reason/claim from the row).

+ +

Developer steps

+ +
    +
  1. Upload via /employee/upload with action deletion; note file_id.
  2. +
  3. If validation fails, use GET /employee/excel_error/{file_id} — look for code 10 (member not in DB).
  4. +
  5. After success, query emp_endorsement where file_id = upload id, actions = 'd', status = 'pending'.
  6. +
  7. If rows were skipped, search logs for not found or Existing endorsement pending.
  8. +
  9. TPA path: trace initializeDeletionProcessForTpaApiData and the generated tpa_auto_deletion_*.xls file.
  10. +
+ +

Common pitfalls

+ +
    +
  • Name mismatch — Excel name must match employees.name exactly (case/spacing).
  • +
  • Not active — only emp_status = active and employee_polices.status = active match.
  • +
  • Duplicate pending deletion — row skipped if a pending deletion endorsement already exists for that policy row.
  • +
  • Self vs dependent — wrong relationship in the row changes scope (one member vs whole family).
  • +
  • File always success after disembarkfiles.status does not reflect per-row skips; use endorsements table + logs.
  • +
  • Not immediate delete — members stay active until endorsements are approved/applied downstream.
  • +
+ +

Related: Inception (onboard pipeline uses the same upload screen and first two validation steps).

diff --git a/app/Views/docs/docs_header.php b/app/Views/docs/docs_header.php index 2b8a8e6e..949ee16e 100644 --- a/app/Views/docs/docs_header.php +++ b/app/Views/docs/docs_header.php @@ -39,6 +39,7 @@ $base = base_url(); /* ─── TOKENS ─────────────────────────────── */ :root { --sidebar-w : 260px; + --toc-w : 200px; --topbar-h : 56px; --bg : #ffffff; --bg2 : #f7f8fa; @@ -143,6 +144,8 @@ $base = base_url(); /* ─── LAYOUT WRAPPER ─────────────────────── */ .docs-layout { display : flex; + width : 100%; + align-items: flex-start; margin-top: var(--topbar-h); min-height: calc(100vh - var(--topbar-h)); } @@ -208,6 +211,15 @@ $base = base_url(); .callout.success { background: #f0fdf4; border-color: #4ade80; color: #15803d; } /* ─── TABLES ─────────────────────────────── */ + .docs-table-wrap { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + margin: 20px 0; + border: 1px solid var(--border); + border-radius: 8px; + } + .docs-table-wrap table { margin: 0; min-width: 640px; } + .docs-table-wrap--fluid table { min-width: 0; width: 100%; } table { width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 13.5px; } th { background : var(--bg2); diff --git a/app/Views/docs/docs_main_open.php b/app/Views/docs/docs_main_open.php index e964f522..e9a75656 100644 --- a/app/Views/docs/docs_main_open.php +++ b/app/Views/docs/docs_main_open.php @@ -27,10 +27,11 @@ $toc = $toc ?? []; L["iterate in DB order"] - L --> K{"rack_rate_name changed"} - K -->|yes| B["open new bucket"] - K -->|no| U["same bucket"] - B --> P["append row to slab_rates array"] - U --> P - P --> G["store grid_master from row"] - - - -
- i -
- When maintaining this helper, inspect the implementation for duplicate pushes on the first row of a new rack name; - downstream code tolerates duplicate slab rows but it can confuse debugging of premium matches. -
-

get_familiy_composition

@@ -321,13 +294,122 @@ flowchart LR (already grouped to one employee). Keys align with the JSON used in rack configuration (additional_relationship), except either-parents-pil and elders_count which are stripped before comparison.

+

+ This function mainly: +

    +
  • Reads all family members
  • +
  • Normalizes relationship names
  • +
  • Builds a summarized family composition object
  • +
  • Maintains counts for: +
      +
    • self
    • +
    • spouse
    • +
    • children
    • +
    • parents
    • +
    • parents-in-law
    • +
    +
  • +
+

flowchart TD - A["single pass over family_data rows"] --> B["slugify column 5 per row"] - B --> C["bump self spouse childrens parents parents-in-law counters"] - C --> D["return family_composition map"] + + START([Start get_familiy_composition]) + + START --> INIT["Initialize family_composition array and slug service"] + + INIT --> LOOP_MEMBERS{"Loop each family member"} + + LOOP_MEMBERS --> GET_REL["Read relationship from row[5]"] + + GET_REL --> SLUGIFY["Slugify relationship"] + + %% SELF + + SLUGIFY --> CHECK_SELF{"relationship == self"} + + CHECK_SELF -->|Yes| SET_SELF_ONE["Set self = 1"] + + CHECK_SELF -->|No| CHECK_SELF_EXISTS{"self key exists?"} + + CHECK_SELF_EXISTS -->|No| SET_SELF_ZERO["Set self = 0"] + + CHECK_SELF_EXISTS -->|Yes| CHECK_SPOUSE + + SET_SELF_ONE --> CHECK_SPOUSE + SET_SELF_ZERO --> CHECK_SPOUSE + + %% SPOUSE + + CHECK_SPOUSE{"relationship == spouse"} + + CHECK_SPOUSE -->|Yes| SET_SPOUSE_ONE["Set spouse = 1"] + + CHECK_SPOUSE -->|No| CHECK_SPOUSE_EXISTS{"spouse key exists?"} + + CHECK_SPOUSE_EXISTS -->|No| SET_SPOUSE_ZERO["Set spouse = 0"] + + CHECK_SPOUSE_EXISTS -->|Yes| CHECK_CHILDREN + + SET_SPOUSE_ONE --> CHECK_CHILDREN + SET_SPOUSE_ZERO --> CHECK_CHILDREN + + %% CHILDREN + + CHECK_CHILDREN{"relationship == son OR daughter"} + + CHECK_CHILDREN -->|Yes| INC_CHILDREN["Increment childrens count"] + + CHECK_CHILDREN -->|No| CHECK_CHILDREN_EXISTS{"childrens key exists?"} + + CHECK_CHILDREN_EXISTS -->|No| SET_CHILDREN_ZERO["Set childrens = 0"] + + CHECK_CHILDREN_EXISTS -->|Yes| CHECK_PARENTS + + INC_CHILDREN --> CHECK_PARENTS + SET_CHILDREN_ZERO --> CHECK_PARENTS + + %% PARENTS + + CHECK_PARENTS{"relationship == father OR mother"} + + CHECK_PARENTS -->|Yes| INC_PARENTS["Increment parents count"] + + CHECK_PARENTS -->|No| CHECK_PARENTS_EXISTS{"parents key exists?"} + + CHECK_PARENTS_EXISTS -->|No| SET_PARENTS_ZERO["Set parents = 0"] + + CHECK_PARENTS_EXISTS -->|Yes| CHECK_INLAWS + + INC_PARENTS --> CHECK_INLAWS + SET_PARENTS_ZERO --> CHECK_INLAWS + + %% PARENTS IN LAW + + CHECK_INLAWS{"relationship == father-in-law OR mother-in-law"} + + CHECK_INLAWS -->|Yes| INC_INLAWS["Increment parents-in-law count"] + + CHECK_INLAWS -->|No| CHECK_INLAW_EXISTS{"parents-in-law key exists?"} + + CHECK_INLAW_EXISTS -->|No| SET_INLAW_ZERO["Set parents-in-law = 0"] + + CHECK_INLAW_EXISTS -->|Yes| NEXT_MEMBER + + INC_INLAWS --> NEXT_MEMBER + SET_INLAW_ZERO --> NEXT_MEMBER + + %% LOOP + + NEXT_MEMBER --> MORE_MEMBERS{"More family members?"} + + MORE_MEMBERS -->|Yes| LOOP_MEMBERS + + MORE_MEMBERS -->|No| RETURN_RESULT + + RETURN_RESULT(["Return family_composition"])
@@ -348,12 +430,87 @@ flowchart TD
flowchart TD - J["decode additional_relationship JSON"] --> U["strip either-parents-pil and elders_count"] - U --> E{"non-NA rules exist"} - E -->|no| F["rack not applicable"] - E -->|yes| K["sequential AND each non-NA key"] - K -->|incoming matches value or any| Y["rack applicable plus relationship tokens"] - K -->|missing key or mismatch| X["rack not applicable"] + + START([Start compare_incoming_family_slab_with_configured_slab]) + + START --> INIT_MAP["Initialize relationship mapping"] + + INIT_MAP --> INIT_RESULT["Initialize result + is_applicable = false + applicable_members = []"] + + INIT_RESULT --> READ_CONFIG["Read configured family composition from slab"] + + READ_CONFIG --> REMOVE_KEYS["Remove ignored keys + either-parents-pil + elders_count"] + + REMOVE_KEYS --> CHECK_CONFIG{"Configured composition has values?"} + + %% EMPTY CONFIGURATION + + CHECK_CONFIG -->|No| RESET_RESULT["Set result as not applicable"] + + RESET_RESULT --> RETURN_RESULT + + %% LOOP START + + CHECK_CONFIG -->|Yes| LOOP_CONFIG{"Loop configured relationships"} + + %% CHECK NA + + LOOP_CONFIG --> CHECK_NA{"Value != NA ?"} + + CHECK_NA -->|No| NEXT_RELATION + + %% CHECK KEY EXISTS + + CHECK_NA -->|Yes| CHECK_KEY_EXISTS{"Relationship exists in incoming composition?"} + + CHECK_KEY_EXISTS -->|No| INVALID_RESULT_1["Set: + is_applicable = false + applicable_members = []"] + + INVALID_RESULT_1 --> BREAK_LOOP + + %% VALUE COMPARISON + + CHECK_KEY_EXISTS -->|Yes| CHECK_MATCH{ + Incoming count == configured count + OR + configured value == any + } + + %% MATCH FOUND + + CHECK_MATCH -->|Yes| SET_APPLICABLE["Set is_applicable = true"] + + SET_APPLICABLE --> MERGE_MEMBERS["Merge mapped relationships into applicable_members"] + + MERGE_MEMBERS --> NEXT_RELATION + + %% MATCH FAILED + + CHECK_MATCH -->|No| INVALID_RESULT_2["Set: + is_applicable = false + applicable_members = []"] + + INVALID_RESULT_2 --> BREAK_LOOP + + %% LOOP CONTROL + + NEXT_RELATION --> MORE_RELATIONS{"More relationships?"} + + MORE_RELATIONS -->|Yes| LOOP_CONFIG + + MORE_RELATIONS -->|No| RETURN_RESULT + + BREAK_LOOP --> RETURN_RESULT + + %% RETURN + + RETURN_RESULT(["Return result array"]) +
@@ -378,13 +535,60 @@ flowchart TD
flowchart TD - A["applicable_members from compare"] --> B["scan family_data by index"] - B --> C{"relationship slug in list"} - C -->|yes| D["record index and age from DOB"] - C -->|no| B - D --> B - B -->|done| E["max_count equals number of indexes"] - E --> F["walk indexes in order first gets acting_self true remainder false"] + + START([Start get_applicable_familiy_members]) + + START --> INIT["Initialize: + index = [] + max_age = [] + max_count = 0"] + + INIT --> INIT_SLUG["Initialize slug service"] + + INIT_SLUG --> LOOP_MEMBERS{"Loop each family member"} + + %% READ RELATIONSHIP + + LOOP_MEMBERS --> READ_REL["Read relationship from family_member[5]"] + + READ_REL --> SLUGIFY["Slugify relationship"] + + %% CHECK APPLICABLE + + SLUGIFY --> CHECK_APPLICABLE{ + Relationship exists in applicable_members? + } + + %% MATCH FOUND + + CHECK_APPLICABLE -->|Yes| STORE_INDEX["Add member index into result.index"] + + STORE_INDEX --> CALCULATE_AGE["Calculate member age from DOB"] + + CALCULATE_AGE --> STORE_AGE["Add age into result.max_age"] + + STORE_AGE --> NEXT_MEMBER + + %% NO MATCH + + CHECK_APPLICABLE -->|No| NEXT_MEMBER + + %% LOOP CONTROL + + NEXT_MEMBER --> MORE_MEMBERS{"More family members?"} + + MORE_MEMBERS -->|Yes| LOOP_MEMBERS + + %% FINAL COUNT + + MORE_MEMBERS -->|No| CALCULATE_COUNT["Set max_count = + count(result.index)"] + + CALCULATE_COUNT --> RETURN_RESULT + + %% RETURN + + RETURN_RESULT(["Return result array"])
diff --git a/app/Views/docs/inception.php b/app/Views/docs/inception.php new file mode 100644 index 00000000..ad406a78 --- /dev/null +++ b/app/Views/docs/inception.php @@ -0,0 +1,389 @@ + + +

+ 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. +

+ +

Overview

+ +
+
+flowchart LR + A["Upload Excel on employee upload"] --> B["excelFileFormatValidation"] + B --> C["excelFileDataValidation"] + C --> D["employeesOnboardPreprocess"] + D --> E["employeesOnboardProcess plus policy_transaction"] + B -->|errors| F["files.status failed"] + C -->|errors| F + D -->|no families inserted| F + D -->|OK| G["files.status success"] +
+
+ +

In short:

+
    +
  • Step 1 — Format: Headers, column count/order, per-cell type and mandatory rules.
  • +
  • Step 2 — Data: Family-level checks (Self row, duplicates, policy terms, DB conflicts).
  • +
  • Step 3 — Preprocess: Premium via rack rates, then insert employees and inception policy transaction.
  • +
  • Large files (> 1 MB) run steps 1–3 as background jobs via JobWorker.
  • +
+ +

Key files and routes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AreaLocation
UIapp/Views/employee_upload.php — client, branch, policy, action Inception, file upload
Upload handlerEmployeeController::employeesUplodWithEvents
Pipeline logicEmployeeServiceController — the three functions on this page
Job dispatchapp/Controllers/JobWorker.php — maps job names to EmployeeServiceController
Column / Excel helpersapp/Helpers/excel_util_helper.phpcheck_columns_name, custom validators
Upload recordfiles table via FileModelstatus, action, reason
PremiumEB rack rate calculationcalculate_premium_new, employeesOnboardProcess
+ +

Routes (group /employee, filter authMVC):

+
    +
  • GET /employee/upload — upload screen
  • +
  • POST /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. +

+ +
+ i +
+ 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. +
+
+ +

Sync vs background jobs

+ + + + + + + + + + + + + + + + + +
File sizeStep 1Steps 2–3
< 1 MBexcelFileFormatValidation runs inline in the upload requestAlways queued: excelFileDataValidationemployeesOnboardPreprocess
≥ 1 MBJob excelFileFormatValidationSame 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. +

+ +

Entry points

+ +

Job chain for inception (and missed_inception / addition / dependent_addition):

+

excelFileFormatValidationexcelFileDataValidationemployeesOnboardPreprocess

+ +

Step 1: excelFileFormatValidation

+ +

Runs on the uploaded sheet before any DB business rules.

+ +
    +
  • Loads file from writable/uploads/excel/{file_name}.
  • +
  • Uses $inception_excel_columns when action is inception (same column set for addition / dependent_addition / missed_inception).
  • +
  • Checks policy has terms and slab rates configured — otherwise fails early.
  • +
  • Validates each data row: mandatory (by action code I), date/mobile formats, allowed lists, custom helpers (DOB, relationship, SI, mobile duplicate, etc.).
  • +
  • Stops at first empty row (treated as end of data).
  • +
+ +

Common format error codes

+ + + + + + + + + + + + + +
CodeMeaning
1Mandatory value missing
2Wrong format (e.g. date, mobile)
3Value not in allowed list
4Custom validation failed (DOB, relationship, SI, etc.)
5File / policy / slab configuration problem
6Column 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.

+ +

files.reason shape (debugging)

+ +

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_type1 = format step, 2 = data step
  • +
  • error_summary — counts per error code (after aggregation)
  • +
  • error_data — keyed by Excel row number (1-based, header is row 1)
  • +
+ +

Step 2: excelFileDataValidation

+ +

Runs after format passes. Groups rows by EMP ID (family) and applies business rules.

+ +
+
+flowchart TD + A["Group rows by emp_id"] --> B{"Self in family?"} + B -->|No| C["Error: Self not found"] + B -->|Yes| D{"Duplicate name in family?"} + D -->|Yes| E["Error: Twofold name"] + D -->|No| F{"Emp code already in DB?"} + F -->|Yes| G["Error: duplicate emp code"] + F -->|No| H{"Dependents match policy terms?"} + H -->|No| I["Dependent conflict errors"] + H -->|Yes| J["Row OK for this family"] +
+
+ +

Typical inception checks:

+
    +
  • Self row required (unless GMC parents policy allows otherwise).
  • +
  • No duplicate names within the same family in the file.
  • +
  • Employee code must not already exist for inception / addition / enrollment.
  • +
  • Dependent rules vs policy_terms (family composition, LGBTQ flag, etc.).
  • +
  • Name + emp id consistency vs database (name_and_empid_check_in_db).
  • +
+ +

On success for inception: queues job employeesOnboardPreprocess. Other actions queue different jobs (deletion, correction, etc.).

+ +

Common data-validation error codes

+ + + + + + + + + + + + +
CodeMeaning
7Duplicate name within same family in the Excel file
9Record already exists in DB (inception / addition)
10Record not found (used on deletion flows)
14Self row missing in family
26Duplicate employee code in file or DB
+ +

Step 3: employeesOnboardPreprocess

+ +

Calculates premium and writes members to the database.

+ +
    +
  1. Reload Excel; group by emp_id.
  2. +
  3. For each family: calculate_premium_new() using policy terms + slab rates + rack config.
  4. +
  5. employeesOnboardProcess() — insert/update employees, employee_polices, create policy_transaction (inception).
  6. +
  7. If at least one family inserted → files.status = success; else failed with rack-rate message.
  8. +
+ +

+ 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). +

+ +

Inception Excel columns

+ +

+ 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. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ColHeaderRequired (inception)Notes
AS.NoYes
BEMP IDYesFamily key
CNAME OF EMP/DEPYes
DDOBYesd-M-Y; age vs relationship checked
EGenderYesM / F (several casings allowed)
FRELATIONSHIPYesSelf, Spouse, Son, Daughter, …
GBASIC COVER SIConditionalValidated against slab when applicable
HDate of CoverageNoMandatory for addition / DA only
IDOJNo
JBasic PayNoUsed when policy terms need it
KBand/GradeNo
LDesignationNo
MPhoneNoMobile format; duplicate check
NEmailNoDuplicate check in file
OPRE EXISTING AILMENTSYes0 or 1
PChange eventNoNot used for pure inception
QDate of exitNoDeletion only
RReason for exitNoDeletion only
SUnitNoMust match branch units when filled
+ +

Sample file: use the download link on the employee upload screen (environment-specific).

+ +

Family row layout example

+ +

+ 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.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RowS.NoEMP IDNAMEDOBGenderRELATIONSHIPBASIC COVER SIPED
1Header row (all 19 columns A–S required in file)
21EMP001Raj Kumar15-Jan-1985MSelf5000000
32EMP001Priya Kumar20-Mar-1988FSpouse5000000
43EMP001Arjun Kumar10-Jun-2015MSon5000000
+ +

Rules illustrated:

+
    +
  • Same EMP ID on rows 2–4 → one family processed in step 3.
  • +
  • Self on row 2 only — row 3 without Self would fail with code 14.
  • +
  • DOB uses d-M-Y; ages are checked against relationship (e.g. Son vs Self).
  • +
  • PED (PRE EXISTING AILMENTS) = 0 or 1 on every member for inception.
  • +
  • BASIC COVER SI must match slab rules when the policy uses SI-based racks (often same amount across the family).
  • +
  • Columns H–S can be blank for inception when not mandatory; phone/email must be unique in the file if filled.
  • +
+ +

+ 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. +

+ +

Developer steps

+ +
    +
  1. Confirm policy has policy_terms JSON and slab/rack rates for the client policy.
  2. +
  3. Upload via /employee/upload with action inception; note file_id in response or files table.
  4. +
  5. If status = failed, call GET /employee/excel_error/{file_id} or read files.reason.
  6. +
  7. Map error_data row keys back to Excel (row 1 = header).
  8. +
  9. If format passes but preprocess fails with rack message, debug calculate_premium_new (see rack-rate doc) and SI/slab config.
  10. +
  11. For stuck inprogress, check job queue / JobWorker logs for the three job names.
  12. +
+ +

Common pitfalls

+ +
    +
  • Policy not ready — missing policy_terms or slab rates fails step 1 with code 5.
  • +
  • Missing Self row — one Self per EMP ID in the file (code 14 in step 2).
  • +
  • Wrong header row — column count or name mismatch (code 5 / 6).
  • +
  • Rack rate / SI — preprocess succeeds only if calculate_premium_new returns data for every family.
  • +
  • File status — watch files.reason JSON for row-level errors after failure.
  • +
diff --git a/app/Views/docs/non-eb-claims.php b/app/Views/docs/non-eb-claims.php new file mode 100644 index 00000000..71aec5c5 --- /dev/null +++ b/app/Views/docs/non-eb-claims.php @@ -0,0 +1,488 @@ + + +

+ 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. +

+ +

End-to-end flow

+ +
+
+flowchart TD + L["GET /non-eb-claim/list"] --> F["Filter sidebar → POST /list"] + F --> T["Table non_eb_claim_list.php"] + T --> A{"Action"} + A -->|Add| N["GET /non-eb-claim/new/50 or new/{policy_type_id}"] + N --> C["POST /non-eb-claim/create"] + C --> AM["sendAutoMailTrigger if template is_auto_mail=1"] + A -->|Row click / View| V["GET /non-eb-claim/view/{id}"] + V --> U["POST /non-eb-claim/update"] + U --> SC{"claim_status_id changed?"} + SC -->|yes| AM2["sendAutoMailTrigger"] + MT["GET /non-eb-claim/mail_template"] --> CRUD["POST crud_mail_template/1|2|3"] + R["GET /non-eb-claim/reports"] --> RP["POST /reports — UI only; see pitfalls"] +
+
+ +

Typical user path:

+
    +
  1. Open claim list (default: open claims excluding settled/closed/rejected/withdrawn).
  2. +
  3. Add claim → form opens with canonical status policy type 50 — see why 50 is hardcoded.
  4. +
  5. On save, optional auto-mail fires if a matching template has is_auto_mail = 1.
  6. +
  7. Open claim from list → edit view with history, messages, files, notes.
  8. +
  9. Change status → allowed next statuses from ticket_claim_status.allowed_status; section visibility updates.
  10. +
  11. Configure templates at /non-eb-claim/mail_template (direct URL; not in main Claims sidebar today).
  12. +
+ +

Why policy type 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. +

+
+ +

Design assumption

+ +

+ 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 50 in + ticket_claim_status (and matching templates under ticket_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. +

+ +

Policy type 50 vs policy selected on the form

+ +

On create (non_eb_claim_form.php):

+
    +
  • Hidden policy_type_id is set from the URL segment (typically 50) and is not updated when the user selects a branch policy.
  • +
  • The UI shows the real product name in 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. +

+ +

Where 50 appears in code (change all if this design changes)

+ +
+ + + + + + + + + + + + +
LocationUsage
app/Config/Routes.phpGET non-eb-claim/newclaimForm/50
app/Views/non_eb_claim_list.phpDataTable Add button → /non-eb-claim/new/50
app/Views/non_eb_claim_form.phpHidden 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)
+
+ +

When a Non-EB product needs a different status set

+ +

+ 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: +

+ +
    +
  1. Database — Add full 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.
  2. +
  3. Routes / entry URL — Stop routing every new claim through 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.
  4. +
  5. List Add button — Replace hardcoded new/50 in + non_eb_claim_list.php with the correct id or dynamic selection.
  6. +
  7. Create form — On policy selection, set hidden #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.
  8. +
  9. Controller logic — Ensure getClaimStatusForPolicyType, + getTemplateDataByTicketID, and filters that assume a single Non-EB status catalog are tested for + the new ticket_type.
  10. +
  11. API — Replace hardcoded 50 in listClaimStatuses() if mobile + clients need per-product statuses.
  12. +
+ +

+ 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. +

+ +

Key files and routes

+ + + + + + + + + + + + + + + + + +
AreaFile / route
Controllerapp/Controllers/NonEbClaimController.php
Modelapp/Models/NonEbTicketMasterModel.php
List + filtersapp/Views/non_eb_claim_search.php, non_eb_claim_list.php
New claimapp/Views/non_eb_claim_form.php
View / editapp/Views/non_eb_claim_edit.php
Mail templatesapp/Views/non_eb_claim_mail_template.php
Reportsapp/Views/non_eb_claim_reports.php
Routesapp/Config/Routes.php — group /non-eb-claim, filter authMVC
ACLapp/Config/Acl.php#^/non-eb-claim# (Claims team roles)
App menuapp/Views/layout/header.php — New / List only (EB mail template link is separate)
+ +

MVC route map

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodRouteControllerPurpose
GET/non-eb-claim/listclaimListSearch page + default open claims
POST/non-eb-claim/listclaimListFilter → HTML partial for DataTable
GET/non-eb-claim/newclaimForm/50New claim (default policy type 50)
GET/non-eb-claim/new/{policy_type_id}claimFormNew claim for selected product
POST/non-eb-claim/createcreateClaimCreate ticket + assets + history + auto-mail
GET/non-eb-claim/view/{id}view_claimEdit layout (non_eb_claim_edit)
POST/non-eb-claim/updateupdateClaimUpdate; auto-mail on status change
GET/non-eb-claim/remove?ticket_id=removeClaimSoft delete (is_active = 0)
GET/non-eb-claim/mail_templatemailTemplateTemplate list + modal CRUD UI
POST/non-eb-claim/crud_mail_template/1crudTemplateSave template
POST/non-eb-claim/crud_mail_template/2crudTemplateFetch one template (edit)
POST/non-eb-claim/crud_mail_template/3crudTemplateSoft delete template
POST/non-eb-claim/note/1crudNoteGet note
POST/non-eb-claim/note/2crudNoteSave note
POST/non-eb-claim/replysaveReplyManual outbound mail + message row
GET/non-eb-claim/reportsclaimReportsReports UI shell
POST/non-eb-claim/reportsclaimReportsNot implemented in controller — see Reports
POST/non-eb-claim/getBranchAndPolicygetBranchAndPolicyByClientIDBranches, policies, contacts for client
POST/non-eb-claim/getVisibleSectionsgetVisibleSectionsAjaxSection keys for status
POST/non-eb-claim/getMoreInfogetMoreInfoTicket row JSON
POST/non-eb-claim/uploadFileuploadFileClaim docs (file or Drive URL)
POST/non-eb-claim/getClaimFilesgetClaimFilesList files for ticket
GET/non-eb-claim/removeFile?id=removeFileSoft delete file
POST/non-eb-claim/saveIRDocssaveIRDocsPersist required-docs JSON on ticket
GET/non-eb-claim/testAutoMail/{id}testAutoMailTriggerDev/test auto-mail (optional)
+
+ +

Access control

+ +

+ 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. +

+ +

List and filter

+ +

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_no
  • +
  • client_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. +

+ +

Create and edit claim

+ +

Create

+ +
    +
  1. GET /non-eb-claim/new/{policy_type_id}getFormData(): ACMs (role 3), clients, insurers, initial claim status for product, visible sections.
  2. +
  3. User selects client → POST getBranchAndPolicy fills branch, policy (Non-EB/Marine only), branch contact.
  4. +
  5. Status change → POST getVisibleSections toggles accordion sections client-side.
  6. +
  7. POST /non-eb-claim/create — validation via getValidationRules(), sanitizeInputArrayAdvanced, date normalization, optional asset file upload.
  8. +
  9. Duplicate guard: same client_id + loss_date + policy_no (if policy set) → HTTP 409-style JSON.
  10. +
  11. On success: insert non_eb_ticket_master, saveAssets(), history row, sendAutoMailTrigger(), redirect to list.
  12. +
+ +

View / edit

+ +

+ 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. +

+ +

Status-driven sections

+ +

+ $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. +

+ +

Auto-mail and placeholders

+ +

Template lookup (NonEbTicketMasterModel::getTemplateDataByTicketID):

+
    +
  • Join ticket_claim_status on ticket’s claim_status_id (or explicit status_id for tests).
  • +
  • Join 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):

+ +
+ + + + + + + + + + + + + + +
TokenTicket 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. +

+ +

Mail template CRUD

+ +

+ 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. +

+ +
+
+flowchart LR + UI["mail_template UI"] --> S1["POST crud_mail_template/1 Save"] + UI --> S2["POST crud_mail_template/2 Fetch"] + UI --> S3["POST crud_mail_template/3 Delete"] + S1 --> DB["ticket_mail_template"] + S2 --> DB + S3 --> DB +
+
+ +

Template fields

+ +
    +
  • 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)
  • +
  • Optional 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). +

+ +

UI actions

+ +
+ + + + + + + + + +
ActionEndpointBodyResult
Add / SavePOST …/crud_mail_template/1Form fields + mail_content + is_auto_mail{ status: bool } → reload
Edit loadPOST …/crud_mail_template/2id{ status, data } opens modal
DeletePOST …/crud_mail_template/3idSoft 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). +

+ +

Notes

+ +

On the edit screen:

+
    +
  • POST /non-eb-claim/note/1id (ticket), optional is_auto_query → fetch active note.
  • +
  • POST /non-eb-claim/note/2 — save note (required 3–1000 chars) via TicketNoteModel.
  • +
+ +

Documents and IR checklist

+ +
    +
  • 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 spreadsheet on ticket: asset_file under writable/uploads/non_eb_asset_files/; loss description required when file uploaded.
  • +
+ +

Reports

+ +

+ 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). +

+ +

Data model (summary)

+ + + + + + + + + + + + + +
TableRole
non_eb_ticket_masterMain claim ticket
non_eb_claim_assetRepeating asset lines per ticket
ticket_claim_statusStatuses per ticket_type (policy type id); trigger_type, allowed_status
ticket_mail_templateTemplates; ticket_type = policy type id
ticket_historyField-level audit (status, ACM, priority, …)
ticket_messagesOutbound mail log
ticket_notesUser notes per ticket
claim_filesAttachments; ticket_type = 2 for Non-EB
+ +

Controller reference

+ +
+ + + + + + + + + + + + + + + + + + + +
MethodUsed for
claimList / claimSearchList UI and filtered HTML
claimForm / getFormDataNew claim form bootstrap
view_claimEdit view
createClaim / updateClaimPersist ticket
getClaimStatusForPolicyType / getVisibleSections*Status dropdown + sections
mailTemplate / crudTemplateTemplate admin
sendAutoMailTrigger / constructMailContentAutomated email
saveReply / getTicketMessageManual email thread
crudNoteNotes
uploadFile / getClaimFiles / removeFile / saveIRDocsDocuments
saveAssets / getAssetsAsset grid
claimHistory / putHistoryAfterInsertAudit trail
getBranchAndPolicyByClientIDClient cascade
claimReportsReports page (GET only today)
testAutoMailTriggerDev preview/send test
+
+ +

Developer checklist

+ +
    +
  1. Ensure ticket_claim_status rows exist per Non-EB/Marine policy_type.id with correct trigger_type and allowed_status.
  2. +
  3. Create mail templates at /non-eb-claim/mail_template with matching ticket_type + trigger_type; enable Auto Mail only where intended.
  4. +
  5. Verify insured email is present before relying on auto-mail.
  6. +
  7. Before changing Add/new URLs: read Policy type 50 — clone statuses + templates in DB and update every hardcoded 50 if a product needs its own workflow.
  8. +
  9. Implement POST branch in claimReports() if reports Generate must work.
  10. +
  11. Claim files: always set ticket_type = 2 in new file-related code paths.
  12. +
  13. ACL: extend #^/non-eb-claim# if new roles need access.
  14. +
+ +

Common pitfalls

+ +
    +
  • Template mismatch: No row in ticket_mail_template for policy type + status trigger_type → no auto-mail and empty reply preview.
  • +
  • Reports POST missing: UI POSTs to /non-eb-claim/reports but controller only loads view on GET.
  • +
  • Duplicate claims: Same client + loss date + policy number blocked on create.
  • +
  • Policy type 50 assumption: Shared status catalog under DB 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.
  • +
  • Menu vs Non-EB templates: Sidebar “Mail Template” points to EB /ticket/mail_template, not Non-EB.
  • +
  • Soft deletes: Remove claim/file/template sets is_active = 0; list queries filter active only.
  • +
  • Asset file: Upload without loss description fails validation on create/update.
  • +
  • REST API separate: Mobile create/list under employeeRest / Api\NonEbClaimApiController — different validation and flows.
  • +
diff --git a/app/Views/docs/non-eb-opportunities.php b/app/Views/docs/non-eb-opportunities.php new file mode 100644 index 00000000..eb4e7576 --- /dev/null +++ b/app/Views/docs/non-eb-opportunities.php @@ -0,0 +1,638 @@ + + +

+ 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. +

+ +

End-to-end flow

+ +
+
+flowchart TD + A["/leads/list — Add → Non-EB"] --> B["Add form leads_non_eb.php"] + B --> C["POST /leads/create"] + C --> D["status: queued"] + D --> E["Action: RFQ"] + E --> F["GET /leads/createRfqSheet"] + F --> G["status: rfq_created"] + G --> H["Action: QCR"] + H --> I["GET /leads/createQcrSheet"] + I --> J["status: qcr_created"] + J --> K["Action: Send Internal Mail"] + K --> L["POST /leads/sendMail internal"] + L --> M["Action: Send Insurer Mail"] + M --> N["POST /leads/sendMail insurer → rfq_sent"] + N --> O["Action: Send Client Mail"] + O --> P["POST /leads/sendMail client → qcr_sent"] + P --> Q["Action: Placement"] + Q --> R["POST /leads/sendMail placement → won"] +
+
+ +

Typical sequence from the list:

+
    +
  1. Create opportunity (form submit) → queued
  2. +
  3. RFQ — create/open Google Sheet → rfq_created
  4. +
  5. QCR — copy RFQ sheet → qcr_created
  6. +
  7. Send Internal Mail — team mail with RFQ or QCR attachment (by current status)
  8. +
  9. Send Insurer Mail — RFQ sheet attached → rfq_sent
  10. +
  11. Send Client Mail — QCR sheet attached → qcr_sent
  12. +
  13. Placement — placement sheet + policy/payment fields → won
  14. +
+ +

+ 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). +

+ +

Key files

+ + + + + + + + + + + + +
AreaFile
List + action dropdownapp/Views/leads_list.php
Non-EB add/edit formapp/Views/leads_non_eb.php
EB vs Non-EB form includeapp/Views/leads_form_handler.php
All backend logicapp/Controllers/LeadsController.php
Google SheetsGoogleSheetLib, Config\RfqConfig
+ +

Google Sheet config

+ +

+ 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. +

+ +

App ↔ Google Drive flow

+ +
+
+flowchart TB + subgraph setup ["One-time / per product setup"] + T["Drive: master RFQ template per product"] + PT["policy_type.misc.rfq_template_sheet_id"] + ENV[".env RFQ_PARENT_FOLDER_ID"] + SA["Service account JSON + share folder/templates"] + end + + subgraph rfq ["RFQ — createRfqSheet"] + R1["Read template ID from policy_type.misc"] + R1 --> C1["Drive copyTemplate → parent folder"] + C1 --> F1["Sheets find/replace placeholders"] + F1 --> P1["Drive applyPermissions viewers"] + P1 --> PR1["Sheets applyProtections"] + PR1 --> S1["Save leads.misc.rfq_sheet_id"] + end + + subgraph qcr ["QCR — createQcrSheet"] + S1 --> C2["Drive copyTemplate from rfq_sheet_id"] + C2 --> P2["applyPermissions viewers"] + P2 --> S2["Save leads.misc.qcr_sheet_id"] + end + + subgraph placement ["Placement — createAndDownloadPlacementSheet"] + S2 --> C3["Drive copyTemplate from qcr_sheet_id"] + C3 --> P3["applyPermissions viewers"] + P3 --> S3["Save leads.misc.placement_sheet_id"] + S3 --> X1["Drive export .xlsx → mail attachment"] + end + + subgraph mail ["Mail — downloadFileFromGoogleSheet"] + S1 --> X2["Export RFQ or QCR as Excel"] + S2 --> X2 + end + + PT --> R1 + ENV --> C1 + T --> PT + SA --> C1 +
+
+ +

Library: app/Libraries/GoogleSheetLib.php — auth via service account JSON at +{project-root}/nhance-ee8d1-e3c5269b1ec7.json, scopes DRIVE + SPREADSHEETS.

+ +

What must be done before RFQ / QCR

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StepBeforeWhy
1Google service account JSON present; account email shared on template folder + each template sheetGoogleSheetLib cannot copy or edit without Drive access
2RFQ_PARENT_FOLDER_ID set in .envTarget folder for every copied RFQ/QCR/Placement file
3Master RFQ Google Sheet template created per product (Fire, Marine, GPA, etc.) with placeholder tokensRFQ copy source — one template per policy_type row
4policy_type.misc JSON updated with rfq_template_sheet_id for that productcreateRfqSheet() reads this; fails with “RFQ template not found” if missing
5Non-EB opportunity created with correct policy_type_id and rfq_qcr_viewers emailsLead must exist; viewers become sheet editors after copy
6Before QCR: RFQ action completed (leads.misc.rfq_sheet_id set)QCR copies the lead’s RFQ sheet, not the policy template
7Before Placement mail: QCR action completed (leads.misc.qcr_sheet_id set)Placement copies the QCR sheet
+ +

Where to configure sheet ID per product

+ +

+ 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;
+ +
+ i +
+ There is no admin UI field for 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:

+
    +
  • From the Google Sheet URL: https://docs.google.com/spreadsheets/d/{FILE_ID}/edit
  • +
  • CLI — list all sheets in the RFQ parent folder: + php 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:

+ + + + + + + + + + +
StageCopy sourceStored on lead
RFQpolicy_type.misc.rfq_template_sheet_idleads.misc.rfq_sheet_id
QCRleads.misc.rfq_sheet_idleads.misc.qcr_sheet_id
Placementleads.misc.qcr_sheet_id (filename: QCR → Placement)leads.misc.placement_sheet_id
+ +

App-level config files

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SettingLocationPurpose
RFQ_PARENT_FOLDER_ID.envConfig\RfqConfig::$rfqParentFolderIdGoogle Drive folder where copied RFQ/QCR/Placement files are created
rfqPlaceholdersapp/Config/RfqConfig.phpMaps template tokens like {{INSURED_NAME}} to lead field keys filled on RFQ create
rfqClaimsPlaceholderapp/Config/RfqConfig.phpDefault {{CLAIMS_DETAILS}} — multi-row claims table from fin_years_claims
protectionsapp/Config/RfqConfig.phpLocked cell ranges on new RFQ sheets only (e.g. RFQ Page!B12:C12)
Service account keynhance-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 templateFilled 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. +

+ +

Step 1 — Create opportunity

+ +

From the list

+
    +
  1. Open /leads/list → click Add.
  2. +
  3. Modal Select Opportunity Type → choose Non-EB (lead_form_type=2).
  4. +
  5. Redirect: GET /util/getLeadNonEB/2/0.
  6. +
+ +

Form (leads_non_eb.php)

+
    +
  • Sections: Opportunity Details, Client Information, Branch Information, Policy Details, Sales & Assignment.
  • +
  • Policy types limited to allocg = Non-EB or Marine.
  • +
  • Policy type change → GET /util/getPolicyTypeFields → dynamic fields in #appendArea.
  • +
  • Renewal types may show claim history rows → stored as fin_years_claims JSON.
  • +
  • rfq_qcr_viewers (emails) become Google Sheet editors later.
  • +
  • Submit → AJAX POST /leads/create with lead_form_type=2.
  • +
+ +

Controller: createLead()

+
    +
  • prepareLeadData()prepareSingleLeadData() (one lead row; EB uses multi-row).
  • +
  • Non-EB validation: policy type, DOC/DOE, claim-history rows when claim_history=1.
  • +
  • insertNewLead() — no demography background job (EB-only).
  • +
  • Initial status: queued (In-Queued).
  • +
+ +

Edit flow

+ +

From the list action menu → Edit (available for both EB and Non-EB):

+ +
    +
  1. JS: getLeadsDataForEdit(lead_id, lead_form_type, actual_lead_id)
  2. +
  3. Redirect: GET /util/getLeadNonEB/{lead_form_type}/{actual_lead_id}/{lead_id}
  4. +
  5. Controller: getLeadNonEB($type, $actual_lead_id, $id) (~line 4818) +
      +
    • Loads master data (issuer, policy types, sales team, etc.)
    • +
    • When $id present: fetches lead_edit_data, files, custom fields, date formatting
    • +
    • Builds dynamic policy HTML via generateViewPageHtml()
    • +
    • Renewal Non-EB: renders rfq/claims_details_non_eb into claims_details_html
    • +
    • Returns layout with leads_form_handler → includes leads_non_eb.php when selected_lead_type != 1
    • +
    +
  6. +
  7. Form pre-fills client, branch, policy, files, viewers, status, lost reason, etc.
  8. +
  9. Submit same endpoint: POST /leads/create with hidden idupdateOldLead()
  10. +
+ +

Steps 2–7 — List table actions (Non-EB)

+ +

+ 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). +

+ +
+ i +
+ Menu order on screen (after opportunity is created): Edit → RFQ → QCR → + Send Internal Mail → Send Insurer Mail → Send Client Mail → Placement → Email History. +
+
+ +

Action → view handler → controller endpoint

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#ActionVisible whenView (JS) / handler classController endpoint(s)Status after
EditAlways (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
1RFQlead_form_type === 2 only.btnRfqSheetListcreateRfqSheetFromList() + GET + /leads/createRfqSheet?lead_id={id}
+ LeadsController::createRfqSheet() — opens Google Sheet URL in new tab +
rfq_created
2QCR + Non-EB; status not queued or rfq_created; + role 1, 5, 2, 3 or Business Support team + .btnQcrSheetListcreateQcrSheetFromList() + GET + /leads/createQcrSheet?lead_id={id}
+ LeadsController::createQcrSheet() +
qcr_created
3Send Internal MailNon-EB; status != queued.btnInternalMailListopenInternalMailFromList() + GET + /leads/mailTemplate?lead_id={id}&template_type=rfq
+ POST + /leads/sendMailrecipient_type=internal +
4Send Insurer MailSame as internal mail.btnInsurerMailListopenInsurerMailFromList() + GET + /leads/mailTemplate?lead_id={id}&template_type=rfq
+ POST + /leads/sendMailrecipient_type=insurer (RFQ sheet attach) +
rfq_sent
5Send Client MailSame as internal mail.btnClientMailListopenClientMailFromList() + GET + /leads/mailTemplate?lead_id={id}&template_type=qcr
+ POST + /leads/sendMailrecipient_type=client (QCR sheet attach) +
qcr_sent
6PlacementSame as internal mail.btnPlacementListopenPlacementFromList() + GET + /rfq/placementData/{id}
+ GET + /leads/mailTemplate?lead_id={id}&template_type=placement
+ POST + /leads/sendMailrecipient_type=placement +
won
Email HistoryAlways (EB + Non-EB).btnHistorygetLeadsDataForMailHistory() + 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 (#external_cc_disclaimer_modal) before client/placement send to non-user emails
  • +
+ +

Step 2 — RFQ (createRfqSheet())

+ +
    +
  1. Load lead + policy_type.misc.rfq_template_sheet_id.
  2. +
  3. If misc.rfq_sheet_id exists → return existing Google Sheet URL.
  4. +
  5. Copy template via GoogleSheetLib::copyTemplate() into RfqConfig::rfqParentFolderId.
  6. +
  7. Fill placeholders (buildRfqPlaceholderData()): client, GST, PAN, policy period, claims table.
  8. +
  9. Grant editors from leads.rfq_qcr_viewers email JSON.
  10. +
  11. Save misc.rfq_sheet_id; set status = rfq_created.
  12. +
  13. UI opens sheet in a new browser tab.
  14. +
+ +

Step 3 — QCR (createQcrSheet())

+ +
    +
  1. Requires misc.rfq_sheet_id — returns 400 if RFQ not created yet.
  2. +
  3. If misc.qcr_sheet_id exists → return existing URL.
  4. +
  5. Copy the RFQ sheet (not the policy template) with a QCR filename.
  6. +
  7. Same editor permissions from rfq_qcr_viewers.
  8. +
  9. Save misc.qcr_sheet_id; set status = qcr_created.
  10. +
+ +

Steps 4–6 — Mail actions

+ +

Load template — 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. +

+ +

Send — sendMailWithAttachement()

+ +

For Non-EB (lead_form_type == 2), the Excel attachment comes from Google Sheets:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
recipient_typeSheet usedStatus after send
internalQCR if status is qcr_created/qcr_sent, else RFQUnchanged
insurermisc.rfq_sheet_idrfq_sent (unless already past that stage)
clientmisc.qcr_sheet_idqcr_sent
placementcreateAndDownloadPlacementSheet() — copies QCR sheetwon + saves placement/payment/installment fields
+ +

Implementation detail: downloadFileFromGoogleSheet() exports the chosen sheet to a temp +.xlsx under writable/tmp/ before MailHelper::send_email().

+ +

Step 7 — Placement

+ +

Opened from list action → openPlacementFromList(leadId):

+
    +
  1. GET /rfq/placementData/{id} — pre-fills policy dates, premium, CD, installments, contacts.
  2. +
  3. GET /leads/mailTemplate?template_type=placement — subject/body/attachments.
  4. +
  5. User fills placement modal: dates, premium/CD/total, installment rows, To/CC, optional external CC.
  6. +
  7. POST /leads/sendMail with recipient_type=placement.
  8. +
  9. Backend copies QCR → placement Google Sheet, attaches Excel, updates lead to won, + persists placement_date, premium_amount, cd_amount, installments, etc.
  10. +
+ +

Status lifecycle

+ + + + + + + + + + + + + + +
StatusLabelSet by
queuedIn-QueuedCreate form (default)
rfq_createdRFQ CreatedcreateRfqSheet()
rfq_sentRFQ SentInsurer mail
qcr_createdQCR CreatedcreateQcrSheet()
qcr_sentQCR SentClient mail
wonWonPlacement mail
lostLostUser sets on edit form + lost reason
+ +

leads.misc JSON

+ +
{
+  "rfq_sheet_id": "…",
+  "qcr_sheet_id": "…",
+  "placement_sheet_id": "…"
+}
+ +

Controller reference (list flow)

+ +

Methods in LeadsController.php used by the list Non-EB flow:

+ + + + + + + + + + + + + + + + + + + + +
MethodTriggered 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)
+ +

Developer checklist

+ +
    +
  1. Complete Google Sheet config for each Non-EB/Marine policy_type before first RFQ.
  2. +
  3. Set policy_type.misc.rfq_template_sheet_id per product (see sheet ID per product).
  4. +
  5. Configure RFQ_PARENT_FOLDER_ID in .env and share folder/templates with the service account.
  6. +
  7. Ensure placeholders in master templates match Config\RfqConfig::$rfqPlaceholders.
  8. +
  9. Gate new list actions with $isNonEb in leads_list.php.
  10. +
  11. Respect sheet order: RFQ → QCR → mails → placement.
  12. +
  13. QCR button: roles [1, 5, 2, 3] or BUSINESS_SUPPORT_TEAM_ID in user team.
  14. +
+ +

Common pitfalls

+ +
    +
  • QCR before RFQ: createQcrSheet fails if rfq_sheet_id is missing.
  • +
  • Queued status: Mail and placement actions are hidden until status moves past queued.
  • +
  • Insurer mail needs RFQ sheet; client mail needs QCR sheet — create sheets before sending.
  • +
  • Internal mail attachment picks RFQ or QCR based on current lead status.
  • +
  • Edit vs create: same POST /leads/create; presence of hidden id triggers update.
  • +
diff --git a/app/Views/docs/partials/docs_header.php b/app/Views/docs/partials/docs_header.php index 2b8a8e6e..949ee16e 100644 --- a/app/Views/docs/partials/docs_header.php +++ b/app/Views/docs/partials/docs_header.php @@ -39,6 +39,7 @@ $base = base_url(); /* ─── TOKENS ─────────────────────────────── */ :root { --sidebar-w : 260px; + --toc-w : 200px; --topbar-h : 56px; --bg : #ffffff; --bg2 : #f7f8fa; @@ -143,6 +144,8 @@ $base = base_url(); /* ─── LAYOUT WRAPPER ─────────────────────── */ .docs-layout { display : flex; + width : 100%; + align-items: flex-start; margin-top: var(--topbar-h); min-height: calc(100vh - var(--topbar-h)); } @@ -208,6 +211,15 @@ $base = base_url(); .callout.success { background: #f0fdf4; border-color: #4ade80; color: #15803d; } /* ─── TABLES ─────────────────────────────── */ + .docs-table-wrap { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + margin: 20px 0; + border: 1px solid var(--border); + border-radius: 8px; + } + .docs-table-wrap table { margin: 0; min-width: 640px; } + .docs-table-wrap--fluid table { min-width: 0; width: 100%; } table { width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 13.5px; } th { background : var(--bg2); diff --git a/app/Views/docs/partials/docs_main_open.php b/app/Views/docs/partials/docs_main_open.php index 8e53fffe..b81630d2 100644 --- a/app/Views/docs/partials/docs_main_open.php +++ b/app/Views/docs/partials/docs_main_open.php @@ -27,10 +27,11 @@ $toc = $toc ?? []; B["excelFileFormatValidation"] + B --> C["excelFileDataValidation"] + C --> D["employeesSIEnhanceProcess job"] + D --> E["Recalc premium plus pending SI endorsements"] + B -->|errors| F["files.status failed"] + C -->|errors| F + D -->|loop done| G["files.status success"] + + + +

In short:

+
    +
  • Step 1 — Format: 5 columns (A–E); Augmented SI validated against slabs (check_si).
  • +
  • Step 2 — Data: Member must exist on active policy; code 10 if not found.
  • +
  • Step 3 — SI enhance: Picks rack rate by relationship, recalculates premium, inserts 3 pending endorsements per member.
  • +
  • One Excel row = one member SI change (not whole-family in one row).
  • +
+ +

Key files and routes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AreaLocation
UIapp/Views/employee_upload.php — action SI Enhancement
UploadEmployeeController::employeesUplodWithEvents
ValidationexcelFileFormatValidation, excelFileDataValidation
SI processemployeesSIEnhanceProcess
Onboard SI pathemployeesSIEnhanceProcessWhileOnbboard — SI during dependent addition onboard (not Excel)
JobemployeesSIEnhanceProcess in JobWorker.php
Helpersexcel_util_helper.phptransform_si_excel_row_to_calculatable_format, premium_calculation_manager
+ +

Routes (group /employee, authMVC):

+
    +
  • GET /employee/upload — upload screen
  • +
  • POST /employee/uploadupload-action-type=si_enhancement
  • +
  • GET /employee/excel_error/{file_id} — validation errors
  • +
+ +
+ i +
+ files.policy_id is the client policy id. Slab rates load via + getPolicySlabRatesForEmpOnboard(policy_id, client_id). +
+
+ +

Sync vs background jobs

+ + + + + + + + + + +
Step< 1 MB≥ 1 MB
Format validationInline on uploadJob excelFileFormatValidation
Data validationJob excelFileDataValidationSame
SI enhanceJob employeesSIEnhanceProcessSame
+ +

Step 1: excelFileFormatValidation

+ +
    +
  • Uses $si_enhance_excel_columns5 columns (A–E).
  • +
  • Action code SI for mandatory rules.
  • +
  • Augmented SI (D): custom check_si against policy slabs (same helper as inception SI column).
  • +
  • Date of SI Enhancement (E): d-M-Y.
  • +
  • Policy must have slab/rack configuration or format step fails early (code 5).
  • +
+ +

Format error codes

+ + + + + + + + + + + + + +
CodeMeaning
1Mandatory missing
2Wrong format
3Not in allowed list
4Custom validation failed (e.g. invalid Augmented SI for slab)
5File / policy / slab problem
6Column headers wrong
+ +

Step 2: excelFileDataValidation

+ +

name_and_empid_check_in_db for si_enhancement:

+
    +
  • Row must match active employee + active employee_polices on the policy.
  • +
  • Not found → error code 10 (“Record Not found”).
  • +
+ +

On success → queues employeesSIEnhanceProcess.

+ +

Step 3: employeesSIEnhanceProcess

+ +
+
+flowchart TD + A["Read row Augmented SI and date"] --> B["Match active employee and policy"] + B --> C{"Pending SI on basic_cover_si?"} + C -->|Yes| D["Skip row log error"] + C -->|No| E["Resolve rack rate by relationship"] + E --> F["premium_calculation_manager"] + F --> G["Insert 3 pending endorsements actions si"] +
+
+ +

Per row:

+
    +
  1. Strip number formatting from Augmented SI (removeNumberFormatting).
  2. +
  3. Load employee (emp code + name + branch) and active employee_polices row.
  4. +
  5. Find applicable slab/rack rate from member relationship (Son/Daughter → childrens, parents, etc.).
  6. +
  7. Build calculatable member payload via transform_si_excel_row_to_calculatable_format (uses family max age/count).
  8. +
  9. premium_calculation_manager → new premium for the augmented SI.
  10. +
  11. Insert pending endorsements on employee_polices (actions = si):
  12. +
+ + + + + + + + + + +
field_namenew_value source
basic_cover_siExcel Augmented SI (column D)
premiumRecalculated premium from rack logic
si_enhancement_dateExcel 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. +

+ +

SI Enhancement Excel columns

+ + + + + + + + + + + + +
ColHeaderRequiredNotes
AS.NoYes
BEMP IDYes
CNAME OF EMP/DEPYesMust match DB
DAugmented SIYesNew SI; validated via check_si
EDate of SI EnhancementYesd-M-Y
+ +

Row layout examples

+ +

Enhance Self SI — one row:

+ + + + + + + + +
EMP IDNAMEAugmented SIDate of SI Enhancement
EMP001Raj Kumar100000019-May-2026
+ +

Enhance spouse only — separate row (premium uses that member’s relationship for rack selection):

+ + + + + + + + +
EMP IDNAMEAugmented SIDate of SI Enhancement
EMP001Priya Kumar50000019-May-2026
+ +

Developer steps

+ +
    +
  1. Confirm slab/rack rates exist for the client policy (same as inception).
  2. +
  3. Upload with upload-action-type=si_enhancement; note file_id.
  4. +
  5. On validation failure, check /employee/excel_error/{file_id} — codes 4 (SI/slab) or 10 (member missing).
  6. +
  7. After success, query emp_endorsement where file_id = upload id, actions = 'si', status = 'pending' — expect up to 3 rows per member (same group_key).
  8. +
  9. Compare new_value on basic_cover_si and premium to Excel and rack expectations.
  10. +
+ +

Common pitfalls

+ +
    +
  • Slab / rack not configured — format step or premium calc fails; relationship must map to a non-zero rack key.
  • +
  • Augmented SI vs slabcheck_si in step 1 must pass before the job runs.
  • +
  • Pending SI already exists — duplicate upload for same member skipped until prior endorsement cleared.
  • +
  • Per-member rows — enhancing whole family requires one row per member (unlike deletion Self = whole family).
  • +
  • File success vs rowsfiles.status = success does not guarantee every row created endorsements.
  • +
  • Not live SI updateemployee_polices.basic_cover_si changes after endorsement processing.
  • +
+ +

+ Related: + Inception, + Correction, + EB rack rate calculation. +