nhance/app/Views/docs/tpa-recon.php
2026-05-18 12:28:19 +05:30

309 lines
18 KiB
PHP

<?php
/**
* TPA Recon — content only
* app/Views/docs/tpa-recon.php
*
* Mirrors EmployeeController + EmployeePolicyModel + EmployeeServiceController behaviour.
*/
?>
<h2 id="what-this-is">What this is</h2>
<p>
<strong>TPA Recon</strong> (reconciliation) compares <strong>Nhance</strong> enrolment data with a
<strong>TPA API dump</strong> stored in <code>tpa_api_data</code> for a given batch file. HR can see
who exists only in Nhance, only in the TPA file, or in both with field differences, then take actions:
inception upload for missing members, direct DB updates for mismatches, and a deletion pipeline when
the TPA marks rows for deletion.
</p>
<p>
The flow is intentionally split across <code>EmployeeController</code> (report, proceed, heavy logic),
<code>EmployeePolicyModel::getTPADataVariationReport</code> (SQL slices), and
<code>EmployeeServiceController</code> (Excel pipelines and queued follow-up jobs). Read this page top
to bottom once; use the checklist at the end when you touch production data.
</p>
<h2 id="glossary">Glossary</h2>
<table>
<thead>
<tr><th>Term</th><th>Meaning here</th></tr>
</thead>
<tbody>
<tr>
<td><code>batch_files.id</code> (often called <code>file_id</code> in code)</td>
<td>The batch row for the TPA import / variation context. <code>tpa_api_data.file_id</code> points at the same id.</td>
</tr>
<tr>
<td><code>tpa_api_data</code></td>
<td>One row per TPA member line for that file: <code>emp_code</code>, <code>name</code>, <code>dob</code>, <code>gender</code>, <code>relation</code>, optional <code>ref</code> (FK to <code>employee_polices.id</code>), <code>rec_type</code>, <code>action_flag_status</code> (e.g. <code>D</code> for deletion intent), <code>is_active</code>.</td>
</tr>
<tr>
<td><code>rec_type</code></td>
<td>Snapshot classification on <code>tpa_api_data</code>: <code>matched</code>, <code>need_to_review</code>, or <code>not_in_nhance</code>. Used to speed up repeat UI loads after the first full compute.</td>
</tr>
<tr>
<td><code>ref</code></td>
<td>When set, links a TPA row to <code>employee_polices.id</code> after strict matching in <code>reconTpaApiDataWithEmployeepolicies</code>.</td>
</tr>
<tr>
<td>Not in TPA</td>
<td>Nhance has an active policy member for the client/policy, but no TPA row with that <code>emp_code</code> in the dump.</td>
</tr>
<tr>
<td>Not in Nhance</td>
<td>TPA has an <code>emp_code</code> not present in the Nhance “master” list for that client/policy (see model method with <code>$all = true</code>).</td>
</tr>
<tr>
<td>Need to review / mismatch</td>
<td>Same <code>emp_code</code> and same relation line can be paired, but name, DOB, or gender differ; or relation-level pairing failed and TPA rows for that code need manual attention.</td>
</tr>
</tbody>
</table>
<h2 id="key-files-routes">Key files and routes</h2>
<ul>
<li><strong>Controller:</strong> <code>app/Controllers/EmployeeController.php</code>
<ul>
<li><code>getTPADataVariationReport($file_id, $type)</code></li>
<li><code>proceedTPADataVariationNextStep($file_id)</code></li>
<li><code>generateEmployeeUploadFromNotInNhance(...)</code> (protected)</li>
<li><code>initializeDeletionProcessForTpaApiData($file_id)</code> — expects <code>['file_id' =&gt; batch_file_id]</code></li>
<li>Helpers: <code>reconcileDbWithTpa</code>, <code>reconTpaApiDataWithEmployeepolicies</code>, <code>updateEmployeeDataFromTpa</code>, <code>exportVariationReportExcel</code></li>
</ul>
</li>
<li><strong>Model slice:</strong> <code>app/Models/EmployeePolicyModel.php</code> → <code>getTPADataVariationReport($client_id, $client_policy_id, $file_id, $emp_codes = [], $all = false)</code></li>
<li><strong>Excel / jobs:</strong> <code>app/Controllers/EmployeeServiceController.php</code> (inception, correction, disembark; queues jobs listed below)</li>
<li><strong>Job routing:</strong> <code>app/Controllers/JobWorker.php</code> maps job names to <code>EmployeeController</code> handlers</li>
<li><strong>Routes</strong> (employee group in <code>app/Config/Routes.php</code>):
<ul>
<li><code>GET employee/getTPADataVariationReport/(:num)</code> — default second segment resolves to download-style behaviour</li>
<li><code>GET employee/getTPADataVariationReportView/(:num)</code> — same action with <code>view</code> type (JSON for UI)</li>
<li><code>GET employee/proceedTPADataVariationNextStep/(:num)</code> — query string <code>?tab=...</code> (see Proceed section)</li>
</ul>
</li>
<li><strong>UI:</strong> <code>app/Views/batch_list.php</code> — download / view variation report links call the routes above</li>
</ul>
<h2 id="end-to-end-flow">End-to-end flow</h2>
<div class="mermaid-wrapper">
<div class="mermaid">
flowchart TB
A[TPA rows in tpa_api_data] --> B[getTPADataVariationReport]
B --> C{rec_type snapshot exists?}
C -->|no| D[Compute and persist rec_type]
C -->|yes| E[Load from rec_type]
D --> F[reconTpaApiDataWithEmployeepolicies]
F --> G[Report UI / Excel]
E --> G
B --> H[not_in_tpa from SQL]
G --> I[proceedTPADataVariationNextStep]
I -->|not_in_nhance| J[Inception Excel + files]
I -->|need_to_review| K[updateEmployeeDataFromTpa]
J --> L[Excel pipelines]
K --> L
L --> M[Queued jobs: sync + recon + deletion init]
</div>
</div>
<p>
“Proceed” runs immediate actions; ref sync and deletion initialization also run as <strong>queued jobs</strong>
after inception or correction Excel workflows complete in <code>EmployeeServiceController</code>.
</p>
<h2 id="variation-report">Variation report — <code>getTPADataVariationReport</code></h2>
<p><strong>Inputs:</strong> <code>$file_id</code> (batch file id), <code>$type</code>:</p>
<ul>
<li><code>view</code> — JSON API response with structured <code>data</code> (or empty array if nothing).</li>
<li><code>download</code> — streams Excel via <code>exportVariationReportExcel</code> (three sheets).</li>
</ul>
<p>
The controller loads <code>batch_files</code> for <code>client_id</code>, <code>client_policy_id</code>, then decides
<strong>compute</strong> vs <strong>cached</strong> mode:
</p>
<ul>
<li><strong>Compute</strong> if there is no existing <code>rec_type</code> snapshot on active <code>tpa_api_data</code> for this file (non-empty <code>rec_type</code> on any row).</li>
<li><strong>Cached</strong> if a snapshot already exists — avoids rewriting <code>rec_type</code> on every page load.</li>
</ul>
<p>
<strong>Compute path — high level:</strong>
</p>
<ol>
<li>Load all active <code>tpa_api_data</code> for <code>file_id</code>; index by <code>emp_code</code>.</li>
<li>Initialize every TPA row id to <code>rec_type = matched</code> as a default.</li>
<li>Load Nhance rows from <code>EmployeePolicyModel::getTPADataVariationReport($client_id, $client_policy_id, $file_id)</code> (default branch: members with <code>tpa_id</code> null — the “not yet linked / review” slice).</li>
<li>For each Nhance row, run <code>reconcileDbWithTpa</code> (see next section). Update <code>rec_type</code> on the matched TPA id(s) accordingly.</li>
<li>Load master <code>emp_code</code> list with <code>getTPADataVariationReport(..., [], true)</code>. Any TPA row whose code is not in that list → <code>not_in_nhance</code>.</li>
<li><code>updateBatch</code> all <code>rec_type</code> values in a transaction.</li>
<li>Call <code>reconTpaApiDataWithEmployeepolicies(['file_id' =&gt; $file_id])</code> to populate <code>ref</code> where possible.</li>
</ol>
<p>
<strong>Cached path:</strong> Reads <code>not_in_nhance</code> and <code>need_to_review</code> rows from <code>tpa_api_data</code> by <code>rec_type</code>,
then rebuilds <code>mismatch_data</code> for the UI without re-running the full reconciliation loop.
</p>
<p>
<strong><code>not_in_tpa</code> (always “live”):</strong> Built from TPA emp_codes for the file and
<code>getTPADataVariationReport(..., $tpa_emp_codes)</code> — Nhance members whose <code>emp_code</code> is not in the TPA set.
This list is <strong>not</strong> driven from the <code>rec_type</code> snapshot by design.
</p>
<p>
The response also includes counts and button flags for the “Not in Nhance” proceed action (inception vs
deletion tallies based on empty <code>ref</code> and <code>action_flag_status === 'D'</code>).
</p>
<h2 id="classifying-rec-type">Classifying <code>rec_type</code> — <code>reconcileDbWithTpa</code></h2>
<p>
Given one Nhance row (<code>$db</code>) and all TPA rows for the same <code>emp_code</code> (<code>$tpaRows</code>), the controller walks TPA rows in order:
</p>
<ul>
<li><strong>Relation gate:</strong> <code>strtolower($db['relationship']) === strtolower($tpa['relation'])</code>. If it does not match, that TPA row is skipped.</li>
<li><strong>First passing row wins.</strong> Compare <code>name</code>, <code>dob</code>, <code>gender</code> (case-insensitive for gender).</li>
<li>Return <code>status = matched</code> with <code>tpa_record</code> and <code>not_matching</code> as the list of differing field names (may be empty = perfect match).</li>
<li>If no TPA row passes the relation gate → <code>status = no_match</code>.</li>
</ul>
<p>
The report layer uses that result to set <code>rec_type</code> on TPA ids: perfect match → <code>matched</code>;
matched with diffs → <code>need_to_review</code>; no relation-level match → mark candidate TPA rows for that
<code>emp_code</code> as <code>need_to_review</code>.
</p>
<h2 id="linking-ref-column">Linking <code>ref</code> — <code>reconTpaApiDataWithEmployeepolicies</code></h2>
<p>
Parameter shape: <code>['file_id' =&gt; $batchFileId]</code> (same id as the TPA batch).
</p>
<ul>
<li>Loads active <code>tpa_api_data</code> for the file. Rows that already have a non-empty <code>ref</code> are skipped.</li>
<li>Builds Nhance candidates: active <code>employee_polices</code> joined to <code>employees</code> for the same <code>client_id</code> / <code>client_policy_id</code>, keyed by <code>emp_code</code>.</li>
<li>For each TPA row still needing <code>ref</code>, finds a candidate where <strong>all</strong> match exactly after normalization:
name, relationship vs relation, dob, gender.</li>
<li>Batch-updates <code>tpa_api_data.ref</code> to the chosen <code>employee_policies.id</code> inside a transaction.</li>
</ul>
<p>
This is stricter than <code>reconcileDbWithTpa</code> (which only compares three fields after relation match)
because it must pick a single policy row to link.
</p>
<h2 id="proceed-next-step">Proceed next step — <code>proceedTPADataVariationNextStep</code></h2>
<p><strong>Route:</strong> <code>GET employee/proceedTPADataVariationNextStep/{file_id}?tab=...</code></p>
<table>
<thead>
<tr><th><code>tab</code></th><th>Behaviour</th></tr>
</thead>
<tbody>
<tr>
<td><code>not_in_nhance</code></td>
<td>Calls <code>generateEmployeeUploadFromNotInNhance</code>. On success, an inception-style file exists in <code>files</code> and format validation has run. Follow-up ref/deletion jobs are not invoked inline here (see background jobs).</td>
</tr>
<tr>
<td><code>need_to_review</code></td>
<td>Calls <code>updateEmployeeDataFromTpa(['batch_file_id' =&gt; (int) $file_id])</code>. This applies TPA-sourced values onto <code>employees</code> for reconciled mismatches (no correction Excel in this path). The handler checks <code>$generationResult['success']</code> (not <code>status</code>).</td>
</tr>
<tr>
<td>other / missing</td>
<td>Still logs “proceed” and returns a generic success message.</td>
</tr>
</tbody>
</table>
<div class="callout info">
<span>i</span>
<div>
<strong>Fix applied:</strong> The <code>need_to_review</code> branch previously passed the wrong parameter shape
to <code>updateEmployeeDataFromTpa</code> and read a non-existent <code>status</code> key. It now passes
<code>batch_file_id</code> and honours <code>success</code>, matching how queued jobs and QA utilities call the same method.
</div>
</div>
<h2 id="not-in-nhance-inception">Not in Nhance → inception file — <code>generateEmployeeUploadFromNotInNhance</code></h2>
<p><strong>Purpose:</strong> Turn TPA-only members into an <strong>Employee Upload with Events</strong> Excel so the normal onboarding pipeline can create them in Nhance.</p>
<ol>
<li>Validate <code>client_id</code>, <code>client_policy_id</code>, <code>client_branch_id</code> on the batch file.</li>
<li>Master codes = <code>getTPADataVariationReport(..., [], true)</code> → <code>emp_code</code> list.</li>
<li>Select active <code>tpa_api_data</code> for this <code>file_id</code> whose <code>emp_code</code> is <strong>not</strong> in the master list.</li>
<li>Build headers from <code>EmployeeServiceController::getInceptionExcelColumns()</code>; map each TPA row (relation synonyms, <code>change_event</code> = <code>addition</code>, dates as <code>d-M-Y</code>, etc.).</li>
<li>Save XLSX under <code>WRITEPATH/uploads/excel/</code>, insert <code>files</code> row (<code>action = addition</code>, <code>status = inprogress</code>).</li>
<li>Run <code>excelFileFormatValidation</code> with <code>['file_id' =&gt; newFileId, 'batch_file_id' =&gt; batchFileId]</code>.</li>
</ol>
<h2 id="need-to-review-sync">Need to review → DB sync — <code>updateEmployeeDataFromTpa</code></h2>
<p>
Expects <code>['batch_file_id' =&gt; int]</code>. Loads the same Nhance slice as the report, loads TPA rows per
<code>emp_code</code>, reuses <code>reconcileDbWithTpa</code>. When status is <code>matched</code> and <code>not_matching</code> is non-empty,
writes allowed fields on <code>employees</code> (name, dob, gender, relationship from TPA relation, corporate email when present).
Returns <code>success</code>, <code>message</code>, and counts in <code>data</code>.
</p>
<h2 id="deletion-initialization">Deletion initialization — <code>initializeDeletionProcessForTpaApiData</code></h2>
<p>
Parameter: <code>['file_id' =&gt; $batchFileId]</code> (same batch / TPA file id).
</p>
<p><strong>Business logic (as implemented):</strong></p>
<ol>
<li>Resolve the batch file; require client, policy, and branch.</li>
<li>From <code>tpa_api_data</code>, select distinct non-empty <code>ref</code> values where <code>file_id</code> matches, rows are active, and <code>action_flag_status = 'D'</code> (deletion intent from TPA).</li>
<li>Those <code>ref</code> values are <code>employee_polices.id</code> values already linked to TPA.</li>
<li>Load <strong>active Nhance members</strong> in the same client/policy scope whose <code>employee_polices.id</code> is <strong>in that set</strong> (<code>whereIn</code> on policy id). These are the rows that will appear on the generated deletion sheet.</li>
<li>Build a deletion-format <code>.xls</code>, create a <code>files</code> row with <code>action = deletion</code>, run <code>EmployeeServiceController::employeeDisembark</code> to create endorsements from the sheet.</li>
<li>Build a separate import-format workbook from export helpers, create a <code>batch_files</code> row, run <code>EmpDataServiceController::importDeletionValidation</code> for the batch import path.</li>
</ol>
<p>
A commented <code>rec_type = matched</code> filter exists in the query; it is intentionally not applied — do not assume
<code>rec_type</code> gates deletion eligibility unless you change the code deliberately.
</p>
<h2 id="background-jobs-chain">Background jobs chain</h2>
<p>
After a successful <strong>inception-style</strong> onboarding from grouped family data
(<code>employeesOnboardProcess</code> path in <code>EmployeeServiceController</code>) when <code>batch_file_id</code> is present in params,
three jobs are enqueued in order:
</p>
<ol>
<li><code>updateEmployeeDataFromTpa</code> — payload includes <code>file_id</code> (newly created processing file where applicable) and <code>batch_file_id</code></li>
<li><code>reconTpaApiDataWithEmployeepolicies</code> — payload <code>['file_id' =&gt; batch_file_id]</code></li>
<li><code>initializeDeletionProcessForTpaApiData</code> — payload <code>['file_id' =&gt; batch_file_id]</code></li>
</ol>
<p>
A similar trio is queued after <code>employeesCorrectionProcess</code> when <code>batch_file_id</code> is passed. This is how
ref linking and deletion initialization catch up <strong>after</strong> Excel-driven workflows finish, even when the
“Proceed” controller path does not call them inline.
</p>
<h2 id="new-developer-checklist">New developer checklist</h2>
<ol>
<li>Identify the <strong>batch file id</strong> you are debugging; confirm matching rows exist in <code>tpa_api_data</code> with the same <code>file_id</code>.</li>
<li>Open <strong>Variation report</strong> in view mode first — inspect <code>not_in_tpa</code>, <code>not_in_nhance</code>, <code>mismatch_data</code> separately.</li>
<li>If <code>rec_type</code> looks stale, remember the controller only recomputes when no snapshot exists unless you clear or adjust <code>rec_type</code> in DB (there is no public “job” route in production docs).</li>
<li>When changing matching rules, update <strong>both</strong> <code>reconcileDbWithTpa</code> and <code>reconTpaApiDataWithEmployeepolicies</code> if they must stay aligned, or document intentional differences.</li>
<li>Before testing deletion flows on real clients, trace <code>action_flag_status</code> and <code>ref</code> on TPA rows — deletion candidates are rows explicitly flagged <code>D</code> with a populated <code>ref</code>.</li>
<li>Watch <code>myLogger</code> entries prefixed with <code>TPA</code> / <code>TPA RECON</code> for operational breadcrumbs.</li>
</ol>
<p>
For QA-only utilities (guarded in production), see internal notes such as
<code>public/dev_logs/2026-04-03.md</code> for <code>updateEmployeeDataFromTpa</code> and related routes.
</p>