# Cursor Prompt — Pull LTD / STD Volume Drivers from EN Census

**Goal:** Extend Plansight's existing EN census pull (which today only feeds the small-group / ACA Medical census) so that **LTD and STD volume calculations** are also seeded from EN — salary, eligibility class, and (for renewals) currently-approved benefit amounts — instead of the broker re-keying them in Plansight's volume uploader. EN is already returning everything we need; Plansight throws it away on ingest.

**Repo:** `Plansight APP` (Laravel) · package: `integrations/EmployeeNavigator/` plus the LTD/STD volume-builder (engineering will need to confirm exact path; see below)

**Effort:** Medium. The EN-side read is trivial (the data is already in the response we discard at `Controller.php:466`). The Plansight-side wiring depends on how the existing volume builder's data contract is shaped — could be a clean slot-in or could need adapters.

---

## Background

Plansight's EN integration was built earliest for **small-group medical / ACA quoting** — the census pull's job was to get an accurate employee-roster + family structure for community-rated medical pricing. That works today. See `integrations/EmployeeNavigator/Controller.php` around line 440-520.

But the `quote-census` response from EN includes per-employee data we currently discard — the same data our LTD/STD volume calculators would otherwise require the broker to upload as a CSV. From `EmploymentCompensation` and `EmployeeCompanyStructureMapping` in the v1 Quote Census schema:

| EN census field | Plansight need |
|---|---|
| `employmentCompensation.annualBenefitSalary.annualBenefitSalary` | LTD/STD volume = sum of insured salary; this is the canonical input |
| `employmentCompensation.annualBenefitSalary.effectiveDate` | Salary as-of date (timing for renewal calcs) |
| `employmentInformation.benefitHireDate` | Eligibility waiting periods, tenure-based class assignment |
| `employmentInformation.jobTitle` | Class identification when classes are job-title based |
| `employmentInformation.benefitWorkState` | Situs / state-specific volume rules (NY/NJ/CA/HI/RI/PR have unique LTD treatments) |
| `companyStructureMapping.eligibilityClass` | Direct class assignment if employer pre-defines classes |
| `companyStructureMapping.division`, `department`, `businessUnit` | Composite class definition when employer slices by org structure |
| `companyStructureMapping.officeLocation` | Location-based class / situs |
| `EmployeeBenefitDetail.disabilityBenefit.approvedBenefitAmount` (per existing enrollment) | Renewal volumes — the actual current insured benefit amount per employee |

Plansight's [audit gap #4](../employee-navigator.html#gap-4) already calls out that hire date, job title, work state, and salary are "exposed but not stored." This project closes that loop specifically for the LTD/STD use case, where the data has the highest direct dollar value (volume drives premium drives commission).

---

## What lands where

### Read side (EN ingestion) — `integrations/EmployeeNavigator/Controller.php` ~line 440-520

Today the per-employee transform produces a `$people[]` row with: `employeeId · relationship · firstName · lastName · gender · birthDate · zip · tobacco`. Add the LTD/STD-relevant fields:

```php
$people[$employeeId] = [
    'employeeId' => $employeeId,
    'relationship' => 'Employee',
    'firstName' => data_get($employee, 'demographics.firstName'),
    'lastName' => data_get($employee, 'demographics.firstInitialLastName'),
    'gender' => $gender,
    'birthDate' => Carbon::parse(data_get($employee, 'demographics.birthDate'))->format('m/d/Y'),
    'zip' => $zip,
    'tobacco' => data_get($employee, 'demographics.tobaccoUseInformation.useTobacco', false),

    // NEW — for LTD/STD volume calculations
    'annualSalary' => data_get($employee, 'employmentCompensation.annualBenefitSalary.annualBenefitSalary'),
    'salaryEffectiveDate' => data_get($employee, 'employmentCompensation.annualBenefitSalary.effectiveDate'),
    'benefitHireDate' => data_get($employee, 'employmentInformation.benefitHireDate'),
    'jobTitle' => data_get($employee, 'employmentInformation.jobTitle'),
    'workState' => data_get($employee, 'employmentInformation.benefitWorkState'),
    'eligibilityClass' => data_get($employee, 'companyStructureMapping.eligibilityClass'),
    'division' => data_get($employee, 'companyStructureMapping.division'),
    'department' => data_get($employee, 'companyStructureMapping.department'),
    'businessUnit' => data_get($employee, 'companyStructureMapping.businessUnit'),
    'officeLocation' => data_get($employee, 'companyStructureMapping.officeLocation'),
];
```

Don't apply these to dependents (they're employee-level only).

### Currently-approved benefit amounts — when renewing existing LTD/STD

For renewal RFPs, also pull `EmployeeBenefitDetail.disabilityBenefit.approvedBenefitAmount` per employee from `memberPlanEnrollments`. This becomes the prior-year volume baseline:

```php
foreach (data_get($employee, 'memberPlanEnrollments', []) as $plan) {
    $approved = data_get($plan, 'employeeBenefit.details.disabilityBenefit.approvedBenefitAmount');
    if ($approved !== null) {
        $people[$employeeId]['approvedDisabilityBenefit'][] = [
            'amount' => $approved,
            'planIdentifier' => data_get($plan, 'memberEnrolledPlan.planIdentifier'),
            'planName' => data_get($plan, 'memberEnrolledPlan.planName'),
        ];
    }
}
```

### Write side (Plansight volume builder)

Engineering should grep the codebase for where the LTD/STD CSV volume uploader lands its data — likely a `Census` / `Volume` / `Roster` model under `app/Models/` or a controller under `app/Http/Controllers/Rest/App/`. The new fields should land in the same shape, either:

- **(Preferred)** Same person row fed into the existing volume model, with the new columns mapped to whatever the volume builder already calls them (`salary`, `class`, `hire_date`, `state`, etc.)
- **(Fallback)** A new "EN-sourced volume rows" table that the volume builder reads alongside the CSV-uploaded rows, with provenance tracking

Pick whichever produces less ceremony. The data is the same regardless of source (CSV upload vs. EN pull) — they should converge into one volume model.

### UI signal

In the existing census-pull UI (under "Pull census from EN" button), add a small status line after a successful pull: **"Pulled N employees · LTD/STD volume seeded from EN salary + class data."** Make it clear to the broker that they don't need to upload a separate CSV for those lines.

---

## What to confirm before writing code

These are explicit unknowns the engineer should resolve before/while implementing — the spec doesn't presume answers:

1. **Where exactly does the existing LTD/STD volume CSV uploader land its data?** Find the model + controller + form, document the path. The new EN-sourced rows should land there.
2. **How does Plansight handle "no salary" for an employee?** EN's `annualBenefitSalary` is optional — if EN doesn't have it, what does the volume builder do today? Probably the broker has to fill it in. Mirror that behavior (don't block the pull on missing salary).
3. **Class normalization.** `eligibilityClass` from EN can be any string the employer defined. Does the existing volume model have a fixed class enum, or is it free-text? If the former, write an explicit map (or a "class not recognized — manual review" warning) rather than silently dropping.
4. **What's the renewal-vs-new behavior?** On a new RFP, no prior approved volumes exist. On a renewal, the `approvedDisabilityBenefit` from EN is the prior-year baseline. Confirm the volume builder differentiates these and that the EN-sourced approved amount is what the renewal calc wants (vs. e.g. the elected amount or the requested amount).
5. **Multi-class plans.** Some employers have LTD plans that vary by class (e.g. exec class gets 60% to $15k/mo, all-other class gets 50% to $5k/mo). The class assignment EN gives us is direct input — but the volume calc has to know which class corresponds to which plan. This is a downstream concern, but flag it for the engineer.

---

## Tests to add

1. **Unit:** Given a fixture EN census response with salary + class + hire date populated, confirm the transformed `$people[]` row has all new fields populated correctly.
2. **Unit (renewal path):** Same fixture but with `memberPlanEnrollments` containing a `disabilityBenefit.approvedBenefitAmount` — confirm the row's `approvedDisabilityBenefit[]` has the amount.
3. **Unit (missing-salary tolerance):** EN response with `annualBenefitSalary` omitted on some employees — confirm the row is built with `annualSalary => null` and the pull doesn't error.
4. **Integration (manual):** Pull a real census in a sandbox group with LTD on the RFP. Confirm the volume calc populates from the pulled salary + class data without requiring a CSV upload.
5. **Coverage:** Spot-check that the EN-sourced row + a CSV-uploaded row converge to the same volume calculation when the same data is provided through both paths.

---

## Non-goals

- **Don't redesign the volume model.** The new fields slot into whatever shape exists today.
- **Don't block on missing fields.** EN's response may have gaps for some employees (no salary, no class). Tolerate them — the broker can still upload a CSV to fill in, or fill in by hand. The promise is "if EN has it, we use it" not "EN must have everything."
- **Don't change the small-group medical census flow.** That's the existing happy path; this work is purely additive (more fields populated on the same row).
- **Don't pull voluntary lines yet.** Voluntary Life / Voluntary AD&D / Critical Illness etc. all have similar volume needs — but those endpoints aren't on the push side yet (see `cursor-prompts/en-add-voluntary-lines.md`). This ticket is scoped to LTD + STD only.

---

## Rollback

Two-step revert:
1. Remove the new fields from the `$people[]` row in `Controller.php` — the transform falls back to today's shape.
2. Revert any volume-builder changes that consumed the new fields. The volume uploader returns to CSV-only.

No migrations introduced (the new fields ride on the existing in-memory row shape, not new DB columns — assuming engineering's research in step 1 above lands on the "same model, more columns" path; if they choose the "new sidecar model" path, that comes with its own minor migration to revert).

---

## Cursor prompt — copy/paste

```
Implement the EN-to-LTD/STD-volume pipe described in
Plansight Dev/Apps/integrations.plansight.com/cursor-prompts/en-pull-ltd-std-volumes.md.

Goal: Plansight's EN census pull at integrations/EmployeeNavigator/Controller.php (~line 440)
currently transforms each EN employee into a $people[] row with name/gender/birthDate/zip/
tobacco only. Add the additional fields needed for LTD and STD volume calculations:

  - annualSalary, salaryEffectiveDate (from employmentCompensation.annualBenefitSalary.*)
  - benefitHireDate, jobTitle, workState (from employmentInformation.*)
  - eligibilityClass, division, department, businessUnit, officeLocation
    (from companyStructureMapping.*)
  - For renewals only: approvedDisabilityBenefit[] from memberPlanEnrollments[].employeeBenefit
    .details.disabilityBenefit.approvedBenefitAmount

Then route those new row fields into Plansight's existing LTD/STD volume builder so the broker
doesn't need to upload a separate volume CSV when EN has the data. Find where the existing
volume uploader lands its data (likely a Census/Volume/Roster model under app/Models/) and
make the EN-sourced rows converge into the same model.

Before writing code:
1. Locate the existing LTD/STD volume CSV uploader and document the model/controller path.
2. Confirm how the volume model handles missing salary today (don't block the pull on
   missing data — tolerate gaps and let the broker fill in).
3. Confirm whether eligibilityClass from EN should be normalized against a Plansight class
   enum (if the model has one) or passed through as free text.
4. Confirm renewal-vs-new differentiation: approvedDisabilityBenefit from EN is the
   prior-year baseline for renewals.

Add an inline status line in the census-pull UI: "Pulled N employees · LTD/STD volume
seeded from EN salary + class data." So the broker knows they don't need to also upload
a CSV for those lines.

Tests:
- Unit: transformed row has all new fields populated from a fixture response
- Unit: renewal path populates approvedDisabilityBenefit[] from memberPlanEnrollments
- Unit (tolerance): missing annualBenefitSalary doesn't error — row is built with null salary
- Integration (manual): sandbox renewal RFP with LTD on it pulls and seeds volumes
  without a CSV upload

Do NOT:
- Touch the small-group medical census flow (it's the existing happy path; we're adding
  fields to the same row, not changing the existing flow)
- Pull voluntary-line volumes (separate ticket — needs the voluntary push side first)
- Redesign the volume model (new fields slot into the existing shape)

When done, summarize: file paths changed, where you found the existing volume uploader,
how the new EN-sourced rows route into it (same model vs. sidecar), and any decisions
made on the four "confirm before writing code" questions above.
```

---

## After this ships

- Update `employee-navigator.html` gap #4 ("Hire date, job title, work state, and salary are exposed but not stored") — partially closed; mark as "Resolved for LTD/STD; still open for medical."
- Update gap #12 (this one) to "Recently shipped" with a green badge.
- Send a release note to brokers: "EN census pulls now seed your LTD/STD volume calculations automatically — no separate CSV needed when EN has salary + class data."
- Re-run `scripts/extract-en-fields.py` from the integrations site repo to refresh the audit. The "fields used by Plansight" count goes up.
