551 lines
19 KiB
PHP
551 lines
19 KiB
PHP
<?php
|
|
/**
|
|
* Visit onboard - content only
|
|
* app/Views/docs/visit-onboard.php
|
|
*
|
|
* Based on:
|
|
* - app/Controllers/EmployeeController.php
|
|
* - app/Controllers/JobWorker.php
|
|
* - app/Config/Routes.php
|
|
* - app/Views/employee_upload.php
|
|
*/
|
|
?>
|
|
|
|
<p>
|
|
Visit onboard is the Visit wellness onboarding flow used for eligible members
|
|
under a selected client policy. The browser first checks how many members are
|
|
still pending, then starts a background job that pages through
|
|
<code>employee_polices</code> rows, groups each family by
|
|
<code>emp_code</code>, sends one payload per family to the external Visit
|
|
API, and stores the returned <code>referenceId</code> back into
|
|
<code>employee_polices.wellness_onboard</code>.
|
|
</p>
|
|
|
|
<div class="callout info">
|
|
<span>i</span>
|
|
<div>
|
|
<strong>Core files</strong>
|
|
The status check, queue trigger, payload builder, API call, and DB update
|
|
logic all live in <code>app/Controllers/EmployeeController.php</code>. The
|
|
queue worker entry is registered in <code>app/Controllers/JobWorker.php</code>.
|
|
</div>
|
|
</div>
|
|
|
|
<h2 id="overview">Overview</h2>
|
|
|
|
<div class="mermaid-wrapper">
|
|
<div class="mermaid">
|
|
flowchart TD
|
|
A[User selects policy] --> B[checkWellnessOnboardStatus]
|
|
B --> C{Pending members > 0}
|
|
C -->|No| D[Hide onboard action]
|
|
C -->|Yes| E[Show Visit onboard action]
|
|
E --> F[initiateWellnessOnboard]
|
|
F --> G[Jobs::addJob]
|
|
G --> H[JobWorker executes initiateWellnessOnboardJob]
|
|
H --> I[Fetch one page of employee policy rows]
|
|
I --> J[Group rows by emp_code]
|
|
J --> K[Build family payload]
|
|
K --> L[POST to Visit API]
|
|
L --> M[Store referenceId in wellness_onboard]
|
|
M --> N{Page was full}
|
|
N -->|Yes| O[Queue next page job]
|
|
N -->|No| P[Batch complete]
|
|
</div>
|
|
</div>
|
|
|
|
<h2 id="prerequisites">Prerequisites</h2>
|
|
|
|
<div class="callout warning">
|
|
<span>!</span>
|
|
<div>
|
|
<strong>A valid Visit plan ID must be mapped to the client policy in the policy edit page before using Visit onboard.</strong>
|
|
Without a proper wellness plan mapping, the current eligibility query will
|
|
not pick that policy for onboarding and the flow will not start as expected.
|
|
</div>
|
|
</div>
|
|
|
|
<p>
|
|
Before testing or using this feature, confirm that the selected client policy
|
|
has a valid <code>wellness_plan_id</code> configured in the policy edit screen.
|
|
This mapping is one of the core prerequisites checked by the current query.
|
|
</p>
|
|
|
|
<h2 id="entry-points">Entry points</h2>
|
|
|
|
<p>
|
|
The current implementation is split across two browser-facing routes and one
|
|
queued job handler:
|
|
</p>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Entry point</th><th>Current responsibility</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>GET checkWellnessOnboardStatus/{client_policy_id}</code></td>
|
|
<td>Counts eligible member rows and returns the number in <code>response.data</code>.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>GET initiateWellnessOnboard/{client_policy_id}</code></td>
|
|
<td>Queues the background job and immediately returns <code>Process started</code>.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>initiateWellnessOnboardJob($arr)</code></td>
|
|
<td>Processes one page, sends family payloads to the Visit API, persists results, and queues the next page if needed.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<pre><code class="language-php">$routes->get("checkWellnessOnboardStatus/(:any)", "EmployeeController::checkWellnessOnboardStatus/$1");
|
|
$routes->get("initiateWellnessOnboard/(:any)", "EmployeeController::initiateWellnessOnboard/$1");
|
|
|
|
'initiateWellnessOnboardJob' => [
|
|
'type' => 'CC',
|
|
'handler' => 'App\Controllers\EmployeeController',
|
|
],</code></pre>
|
|
|
|
<p>
|
|
On the admin page, <code>app/Views/employee_upload.php</code> calls the count
|
|
endpoint when a policy is selected, shows the CTA only when the count is
|
|
greater than zero, and asks for user confirmation before calling the initiate
|
|
endpoint.
|
|
</p>
|
|
|
|
<div class="callout warning">
|
|
<span>!</span>
|
|
<div>
|
|
<strong>Current trigger style</strong>
|
|
The existing implementation starts work from a browser GET route and then
|
|
hands off the heavy work to the queue. If you change the route method or
|
|
path, update <code>Routes.php</code>, the frontend AJAX calls, and any ACL
|
|
expectations together.
|
|
</div>
|
|
</div>
|
|
|
|
<h2 id="eligibility-rules">Eligibility rules</h2>
|
|
|
|
<p>
|
|
Both <code>checkWellnessOnboardStatus()</code> and
|
|
<code>initiateWellnessOnboardJob()</code> use nearly the same base query. A
|
|
member is considered eligible only when all of these conditions are true:
|
|
</p>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Condition</th><th>Meaning in current code</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>employee_polices.client_policy_id = {selected id}</code></td>
|
|
<td>The job is always scoped to one chosen client policy.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>employee_polices.is_active = 1</code></td>
|
|
<td>Only active employee policy rows are considered.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>employee_polices.status = 'active'</code></td>
|
|
<td>Inactive policy-members are excluded.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>employee_polices.wellness_onboard = '0'</code></td>
|
|
<td>Already onboarded rows are skipped because this column later stores the Visit <code>referenceId</code>.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>employees.emp_status = 'active'</code> and <code>employees.is_active = 1</code></td>
|
|
<td>Only active employees/dependants are sent.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>cp.wellness_plan_id</code> is present</td>
|
|
<td>The selected policy must have a wellness plan configured.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>cp.wellness_vendor_id</code> is null, empty, or <code>0</code></td>
|
|
<td>This is how the current code filters policies for this flow today.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>cp.policy_status = 1</code> and <code>cp.is_active = 1</code></td>
|
|
<td>The client policy itself must be active.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<p>
|
|
The status endpoint returns a count of matching member rows, not a count of
|
|
grouped families. That is why the button text says employees, while the job
|
|
later sends one API payload per family group.
|
|
</p>
|
|
|
|
<h2 id="async-flow">Async flow</h2>
|
|
|
|
<ol class="steps">
|
|
<li>
|
|
<strong>Check the pending count</strong>
|
|
<p><code>checkWellnessOnboardStatus($client_policy_id)</code> runs the eligibility query and returns <code>count($data)</code>.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Queue the first job</strong>
|
|
<p><code>initiateWellnessOnboard($client_policy_id)</code> inserts a job with the name <code>initiateWellnessOnboardJob</code> and payload <code>['client_policy_id' => ...]</code>.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Worker resolves the handler</strong>
|
|
<p><code>JobWorker::$event_class_mapping</code> maps that job name back to <code>EmployeeController</code>.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Process one page of rows</strong>
|
|
<p>The job defaults to <code>page = 1</code>, <code>per_page = 50</code>, and calculates the SQL offset from those values.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Group the current page by family</strong>
|
|
<p>Rows are grouped by <code>emp_code</code> before building API payloads.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Send to Visit and persist the response</strong>
|
|
<p>The job posts each family payload, then stores the returned <code>referenceId</code> into all member rows for that family.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Queue the next page only when needed</strong>
|
|
<p>If the current query returns exactly <code>per_page</code> rows, the job assumes more data may exist and queues <code>page + 1</code>.</p>
|
|
</li>
|
|
</ol>
|
|
|
|
<pre><code class="language-php">Jobs::addJob([
|
|
'job_name' => 'initiateWellnessOnboardJob',
|
|
'payload' => [
|
|
'client_policy_id' => $client_policy_id,
|
|
'page' => $page + 1,
|
|
'per_page' => $perPage,
|
|
]
|
|
]);</code></pre>
|
|
|
|
<p>
|
|
The legacy method <code>initiateWellnessOnboardJobOLD()</code> still exists in
|
|
the controller, but the active queue mapping points to the current
|
|
<code>initiateWellnessOnboardJob()</code> implementation.
|
|
</p>
|
|
|
|
<h2 id="family-payload">Family payload</h2>
|
|
|
|
<p>
|
|
The job builds one outbound payload per <code>emp_code</code>. The first row in
|
|
the family is used as the policy-level reference, and every family member
|
|
becomes one entry in <code>memberDetails</code>.
|
|
</p>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Outbound field</th><th>Source in current code</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><code>policyDetails.policyNumber</code></td><td><code>cp.policy_no</code></td></tr>
|
|
<tr><td><code>policyDetails.employeeId</code></td><td><code>emp_code</code></td></tr>
|
|
<tr><td><code>policyDetails.policyName</code></td><td>Hardcoded <code>GMC</code></td></tr>
|
|
<tr><td><code>policyDetails.policyStartDate</code></td><td><code>cp.policy_start_date</code></td></tr>
|
|
<tr><td><code>policyDetails.policyEndDate</code></td><td><code>cp.policy_end_date</code></td></tr>
|
|
<tr><td><code>policyDetails.plan</code></td><td><code>cp.wellness_plan_id</code></td></tr>
|
|
<tr><td><code>policyDetails.source</code></td><td>Hardcoded <code>NHANCE</code></td></tr>
|
|
<tr><td><code>policyDetails.employer</code></td><td><code>clients.short_name</code></td></tr>
|
|
<tr><td><code>memberDetails[].memberId</code></td><td><code>employee_polices.id</code></td></tr>
|
|
<tr><td><code>memberDetails[].relationshipName</code></td><td>Mapped by <code>mapRelationship()</code></td></tr>
|
|
<tr><td><code>memberDetails[].gender</code></td><td><code>M => Male</code>, otherwise <code>Female</code></td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<pre><code class="language-php">[
|
|
'policyDetails' => [
|
|
'policyNumber' => $primary['policy_no'],
|
|
'employeeId' => $empCode,
|
|
'policyName' => 'GMC',
|
|
'policyStartDate' => $primary['cp_policy_start_date'],
|
|
'policyEndDate' => $primary['policy_end_date'],
|
|
'plan' => $primary['wellness_plan_id'],
|
|
'source' => 'NHANCE',
|
|
'employer' => $primary['short_name'],
|
|
'employeeCode' => $empCode,
|
|
],
|
|
'memberDetails' => [
|
|
[
|
|
'memberId' => $row['id'],
|
|
'name' => $row['name'],
|
|
'phone' => $row['mobile'],
|
|
'email' => $row['email_corporate'],
|
|
'relationshipName' => $this->mapRelationship($row['relationship'], $row['gender']),
|
|
'gender' => $row['gender'] == 'M' ? 'Male' : 'Female',
|
|
'dob' => $row['dob'],
|
|
],
|
|
],
|
|
]</code></pre>
|
|
|
|
<div class="callout warning">
|
|
<span>!</span>
|
|
<div>
|
|
<strong>Important mapping rule</strong>
|
|
<code>mapRelationship()</code> throws an exception for
|
|
<code>spouse</code> when gender is missing. If spouse data is incomplete, the
|
|
job can fail before the API call is made.
|
|
</div>
|
|
</div>
|
|
|
|
<h2 id="api-integration">API integration</h2>
|
|
|
|
<p>
|
|
<code>sendFamiliesToWellnessApi()</code> uses the CI4 cURL service and reads
|
|
its runtime configuration from environment values:
|
|
</p>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Env key</th><th>Usage</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>WELLNESS_ONBOARD_ENDPOINT_URL</code></td>
|
|
<td>Target URL for the Visit onboarding POST request.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>WELLNESS_ONBOARD_AUTHORIZATION</code></td>
|
|
<td>Basic auth token value appended to the <code>Authorization</code> header.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<p>
|
|
Each family is posted as JSON with <code>http_errors = false</code> and a
|
|
<code>30</code> second timeout. The helper stores the raw API result back onto
|
|
the in-memory family array under <code>apiResponse</code> so the next step can
|
|
decide whether to persist anything.
|
|
</p>
|
|
|
|
<pre><code class="language-php">$response = $client->post($endpointUrl, [
|
|
'headers' => [
|
|
'Content-Type' => 'application/json',
|
|
'Authorization' => 'Basic ' . getenv('WELLNESS_ONBOARD_AUTHORIZATION'),
|
|
],
|
|
'body' => json_encode($family),
|
|
'http_errors' => false,
|
|
'timeout' => 30,
|
|
]);</code></pre>
|
|
|
|
<p>
|
|
If the HTTP client throws, the exception is captured into
|
|
<code>apiResponse['error']</code> with a synthetic <code>statusCode</code> of
|
|
<code>0</code>.
|
|
</p>
|
|
|
|
<h2 id="response-examples">Response examples</h2>
|
|
|
|
<div class="callout info">
|
|
<span>i</span>
|
|
<div>
|
|
<strong>Sample data only</strong>
|
|
The JSON below uses placeholder IDs, names, phones, and emails so no live
|
|
user data appears in documentation. Production responses follow the same
|
|
shape with real values.
|
|
</div>
|
|
</div>
|
|
|
|
<p>
|
|
<strong>Visit success response</strong> (HTTP 2xx with a body the job can
|
|
decode). <code>updateWellnessOnboardResponseToDB()</code> reads
|
|
<code>referenceId</code> from the decoded JSON and writes it to
|
|
<code>employee_polices.wellness_onboard</code> for each
|
|
<code>policyDetails[].memberId</code> (employee policy row id).
|
|
</p>
|
|
|
|
<pre><code class="language-json">{
|
|
"message": "success",
|
|
"body": "The policy details are posted successfully",
|
|
"policyDetails": [
|
|
{
|
|
"memberId": "100001",
|
|
"name": "Primary Member Example",
|
|
"phone": "9000000001",
|
|
"email": "primary.member@example.com",
|
|
"relationshipName": "husband",
|
|
"gender": "Male",
|
|
"dob": "1962-11-25"
|
|
},
|
|
{
|
|
"memberId": "100002",
|
|
"name": "Dependent Member Example",
|
|
"phone": "9000000001",
|
|
"email": "dependent.member@example.com",
|
|
"relationshipName": "son",
|
|
"gender": "Male",
|
|
"dob": "1996-02-13"
|
|
}
|
|
],
|
|
"referenceId": "00000000000000000000-NHANCE-1700000000123"
|
|
}</code></pre>
|
|
|
|
<p>
|
|
<strong>Visit failure responses</strong> (same endpoint; when validation or
|
|
business rules fail, the body typically looks like one of these). In these
|
|
cases the current persistence step skips updating rows because there is no
|
|
<code>referenceId</code>.
|
|
</p>
|
|
|
|
<pre><code class="language-json">{
|
|
"message": "failed",
|
|
"errorMessage": "Invalid name"
|
|
}</code></pre>
|
|
|
|
<pre><code class="language-json">{
|
|
"message": "failed",
|
|
"errorMessage": "Invalid mobileno"
|
|
}</code></pre>
|
|
|
|
<pre><code class="language-json">{
|
|
"message": "failed",
|
|
"errorMessage": "invalid [\"null\",\"string\"]: 100010"
|
|
}</code></pre>
|
|
|
|
<p>
|
|
The last shape is a schema-style rejection: the bracketed part describes the
|
|
expected type(s), and the trailing id is the member identifier the API could
|
|
not accept (shown here as a placeholder id, not production data).
|
|
</p>
|
|
|
|
<h2 id="database-updates">Database updates</h2>
|
|
|
|
<p>
|
|
Successful persistence is done by
|
|
<code>updateWellnessOnboardResponseToDB()</code>. The method expects the Visit
|
|
API response to contain a <code>referenceId</code>. That value becomes the new
|
|
<code>wellness_onboard</code> value for every member in the family.
|
|
</p>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Before</th><th>After successful onboard</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>employee_polices.wellness_onboard = '0'</code></td>
|
|
<td><code>employee_polices.wellness_onboard = {referenceId}</code></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<p>
|
|
The code collects all row updates for the page and writes them in one
|
|
<code>updateBatch(..., 'id')</code> call, using each
|
|
<code>memberDetails[].memberId</code> as the primary key.
|
|
</p>
|
|
|
|
<pre><code class="language-php">$allUpdates[] = [
|
|
'id' => $memberPk,
|
|
'wellness_onboard' => $referenceId,
|
|
];
|
|
|
|
$this->employeePolicyModel->updateBatch($allUpdates, 'id');</code></pre>
|
|
|
|
<h2 id="failure-behavior">Failure behavior</h2>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Condition</th><th>Current behavior</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>Missing <code>client_policy_id</code> in job payload</td>
|
|
<td>Logs an error and returns <code>true</code> so the worker can continue.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>No rows found for a page</td>
|
|
<td>Logs batch completion and stops queue recursion.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Row missing <code>emp_code</code></td>
|
|
<td>Skips that row and logs a warning.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>HTTP status <code>400</code>, <code>500</code>, or missing response data</td>
|
|
<td>Logs the API error and does not update <code>wellness_onboard</code>.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Missing <code>referenceId</code> in API response</td>
|
|
<td>Logs the issue and skips DB persistence for that family.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Queue context return value</td>
|
|
<td>The job returns <code>true</code> instead of using <code>$this->respond()</code>.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="callout info">
|
|
<span>i</span>
|
|
<div>
|
|
<strong>Current logging style</strong>
|
|
The implementation uses multiple <code>log_message('error', ...)</code>
|
|
calls for progress tracing, not only for failures. Keep that in mind while
|
|
reading logs during QA or production support.
|
|
</div>
|
|
</div>
|
|
|
|
<h2 id="developer-steps">Developer steps</h2>
|
|
|
|
<ol class="steps">
|
|
<li>
|
|
<strong>Keep the browser trigger and route definitions in sync</strong>
|
|
<p>If you rename the route or change the method, update both <code>Routes.php</code> and the AJAX calls in <code>employee_upload.php</code>.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Do not move the heavy loop back into the web request</strong>
|
|
<p><code>initiateWellnessOnboard()</code> should remain a thin queue trigger. The batching work belongs in the job handler.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Preserve family grouping assumptions</strong>
|
|
<p><code>emp_code</code> is the family key. If source data changes, make sure all related members still group together correctly.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Validate required member data before rollout</strong>
|
|
<p>Fields such as <code>emp_code</code>, <code>relationship</code>, <code>gender</code>, <code>dob</code>, <code>mobile</code>, and <code>email_corporate</code> affect payload quality.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Keep the queue mapping intact</strong>
|
|
<p>If you rename <code>initiateWellnessOnboardJob</code>, update the corresponding entry in <code>JobWorker::$event_class_mapping</code>.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Verify runtime configuration before testing</strong>
|
|
<p>The queue worker must be running, and both wellness environment variables must be present before QA can validate the end-to-end flow.</p>
|
|
</li>
|
|
</ol>
|
|
|
|
<h2 id="common-pitfalls">Common pitfalls</h2>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Pitfall</th><th>Why it happens</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>The Visit onboard button never appears</td>
|
|
<td>The status endpoint returned <code>0</code> because the selected policy failed one of the eligibility filters.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>User sees <code>Process started</code> but no records change</td>
|
|
<td>The initial request only queues the job; no worker means no real processing.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Only some members get updated</td>
|
|
<td>Families with API errors, missing <code>referenceId</code>, or missing <code>emp_code</code> are skipped during persistence.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Spouse records fail unexpectedly</td>
|
|
<td><code>mapRelationship()</code> requires gender to translate spouse into <code>husband</code> or <code>wife</code>.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Count shown in UI does not equal number of API calls</td>
|
|
<td>The UI count is member-row based, but outbound requests are family-group based.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Pagination changes create odd onboarding batches</td>
|
|
<td>The job paginates raw rows first and groups families afterward, so careless query changes can alter how families are chunked.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|