+
Bidirectional integration with Employee Navigator. Push employer profiles, plan elections, and rates into EN. Pull census and enrollment data back for ACA quoting. Built like BenefitPoint — same OAuth pattern, same operation framework, same broker-controlled flow.
A broker activates the integration on a Plansight employer, links the employer to its EN counterpart once, and from then on plans push out and census pulls back with a click.
Toggle on under Edit Employer → Employee Navigator. A new "Employee Navigator" tab appears for that employer. Activation is per-employer, not per-brokerage — brokers control which employers sync.
First push prompts EN OAuth login. After the broker links the Plansight employer to an EN company (existing or new), all subsequent pushes / pulls happen without re-auth — system-level token handles ongoing operations.
From Plansight's Benefit History or post-election views, broker selects elected plans and pushes to EN. Plan design, rates (composite or age-banded), benefit summary all flow over. EN side accepts as new plans or renews existing ones by matching benefit type + dates.
Inside an RFP marked community-rated, broker can pull EN census directly at the quoting / census step — employee roster, demographics, current enrolled plan. Skips the spreadsheet upload entirely.
Each side of the integration eliminates a manual step in the renewal workflow.
No more re-keying employer data into EN, no more uploading censuses for community-rated quotes, no more manually entering elected plan details on the EN side at renewal close.
One canonical record of an employer that lives in both systems. Reduces drift between Plansight's quote/election history and EN's enrollment record.
Push elected plans the moment elections close. Census comes back instantly when starting next year's RFP. Cuts hours off both ends of the renewal cycle.
The EN integration handles two distinct flows: employer + plan push (Plansight → EN) when elections close, and census pull (EN → Plansight) when starting an ACA / community-rated quote. They share auth and data-link plumbing but solve different broker problems and run at different points in the year.
Connecting a Plansight employer to an Employee Navigator company. Done once; all future pushes / pulls run on the persisted link.
From the Plansight employer page, Edit Employer → Employee Navigator. Turn on, save. A new Employee Navigator tab appears for that employer.
From the new tab, click push. Plansight redirects to EN's OAuth login. Broker authenticates with their EN credentials. Plansight calls POST /api/Quoting/companies/{groupId}/integration (the CompanyIntegration operation). EN receives the request and creates a Pending Integration on its dashboard.
Broker switches to EN, opens the pending integration. Either links to an existing EN company OR creates a new one (broker enters state + SIC code). EN persists the link.
Broker returns to Plansight's EN tab, refreshes status. Plansight calls GET /api/Quoting/companies/{groupId}/status. EN responds with Accepted. Connection is now live for this employer — both sides know which Plansight group maps to which EN company.
EN companies and Plansight employers don't share a stable external ID. The broker is the only one who knows whether "ACME Industries" in Plansight is the same as "ACME Industries Inc." in EN, or whether to spin up a new EN company. The link step puts that decision in the broker's hands once, then the system tracks the pairing forever.
Once the employer is linked, pushing elected plans is a one-click operation. Plan design, rates, and benefit summary all transfer.
After an RFP closes and elections are in, broker views the elected plans in Plansight. Selects one or more plans → Send to Employee Navigator.
For each selected plan, Plansight builds the benefit details payload from the Quote record and POSTs to EN's notification endpoint. Multiple plans push in a single request batch with error aggregation per plan.
On the EN dashboard, broker sees N new Plan Notifications (one per pushed plan). Each shows carrier, plan name, plan type, dates, and a preview of benefit summary + rates.
Broker clicks into each notification. EN matches by benefit type + date range: if there's a plan with the same benefit type and adjacent dates, EN offers Renew; otherwise Add as new plan. Renewal carries forward eligibility, contributions, and rate history from the previous year.
Plan design, benefit summary, and rates (composite or age-banded — preserved as-is) are all in EN's record. Broker can edit anything that needs to differ between Plansight's quote and EN's enrollment configuration (e.g., eligibility rules).
Plan name, carrier, dates, plan type · benefit summary (deductibles, copays, coinsurance, OOP max) · contribution structure · composite rates OR age-banded rates · funding type · network info.
Eligibility classes (broker sets these in EN at acceptance) · enrollment data (this flows the other direction) · employer-specific contribution overrides not captured in the Plansight quote.
EN matches incoming plans by benefit type + date adjacency. If matched, plan history (eligibility, contributions, rates from prior year) carries forward. If not, plan is added fresh — broker fills eligibility / contribution rules from scratch.
When starting an RFP marked community-rated, the census step can pull directly from EN — no spreadsheet upload, no manual entry of demographics.
Standard RFP setup — broker sets up the RFP and marks it for community-rated quoting (small group / ACA market). The integration is only available for community-rated workflows where individual demographics drive premiums.
At the quoting / census section, broker picks "Pull from Employee Navigator." Plansight calls EN's QuoteCensus operation, which returns the employer's current employee roster + dependents + currently enrolled plans + enrollment counts per plan.
EN returns N plans (e.g., $250 deductible, $750, $1000) with enrollment counts per plan. Broker maps each EN plan to a Plansight option slot (option 1 / 2 / 3) — typically by deductible tier or some other ordering. This is just a mapping decision; no data is being transformed.
Plansight builds the census from EN data: each employee + their demographics (gender, birthDate, ZIP, tobacco status) + which option they're enrolled in. Quote rates calculate against the actual census without any spreadsheet upload.
Plansight maps several EN fields when building the census:
gender · birthDate · zip (truncated to 5 digits) · tobacco (defaults to false; copied from employee to dependent if dependent value is missing)relationship (employee / spouse / domestic partner / child) → Plansight relationship enumOAuth 2.0 with two grant types — authorization code for broker-initiated actions, client credentials for system-level operations.
Used when a broker pushes an employer or pushes plans. Plansight redirects to EN's identity provider, broker authenticates, EN redirects back to /integrations/employeeNavigator/callback. Token cached with TTL = expiry minus 60 seconds.
Used for census polling and status checks that don't require broker interaction. System token is cached separately and reused across requests until expiry.
| Setting | Value / Source |
|---|---|
| OAuth scopes | openid · EnApi · QuotingCompanyIntegrationApi · QuotingPushNotificationApi |
| Identity URL (production) | identity.employeenavigator.net |
| Identity URL (QA) | identity.employeenavigator.net (QA tenant) |
| API base URL | www.employeenavigator.com |
| Credentials storage | config/services.employeeNavigator |
| HTTP client | Symfony NativeHttpClient (registered as Laravel singleton) |
| Token cache | Laravel cache, tagged integrations, TTL = expiry − 60s |
| Session pairing | Keyed session state pairs OAuth callback to the original action (groupPush / quotePush / censusPull) |
All source paths relative to the plansight repo root.
| File | Purpose |
|---|---|
app/Providers/EmployeeNavigatorClientProvider.php | Service provider — registers Symfony NativeHttpClient as injectable singleton (lines 20–22) |
app/integrations/EmployeeNavigator/Api.php | Core request dispatcher. Auth headers, body serialization, response parsing, error logging (lines 155–221) |
app/integrations/EmployeeNavigator/TokenService.php | OAuth state machine. Authorization URL generation (70–102), user token exchange (206–252), client-credentials token fetch (149–198) |
app/integrations/EmployeeNavigator/Controller.php | HTTP entry points — callback handler (35–124), groupPush() (133–205), quotePush() (214–336), pullCensus() (345–578) |
app/integrations/EmployeeNavigator/Operations/CompanyIntegration.php | Operation class for employer integration (push employer) |
app/integrations/EmployeeNavigator/Operations/QuoteNotification.php | Operation class for plan/quote pushes. Builds benefit details payload (line 790) |
app/integrations/EmployeeNavigator/Operations/QuoteCensus.php | Operation class for census retrieval. Orchestrates employee/dependent extraction (line 145) |
app/integrations/EmployeeNavigator/routes.php | 7 routes loaded into routes/web.php — callbacks, status queries, push/pull endpoints |
All EN interactions implement an Operation interface — pluggable request, response, and post-processing handlers. Adding a new operation doesn't duplicate HTTP plumbing.
OAuth callback uses a keyed session entry to pair the auth return with the original user action. No interim DB record needed; flow stays seamless.
Token cache uses Laravel's tagged cache with the integrations tag. One call invalidates EN tokens (and any other integrations on the same tag) wholesale.
Quote and employer pushes are synchronous with immediate modal alerts — no background queue. Trade-off: simpler UX, but a slow EN response blocks the broker on screen.
The push side covers six benefit types. The pull (census) side intentionally only processes Medical — every other product line that EN sends in the census payload is filtered out. The asymmetry is itself a gap and is captured below.
| Benefit type | Push to EN (QuoteNotification) | Pull from EN (QuoteCensus) | Notes |
|---|---|---|---|
| Medical | ✓ Synced | ✓ Synced | Only line that's bi-directional today. |
| Dental | ✓ Synced | — filtered out | Plan + rates push, but enrollments are dropped on import. |
| Vision | ✓ Synced | — filtered out | Same — pushed, not pulled. |
| Short-Term Disability | ✓ Synced | — filtered out | Composite + age-banded + ASO flat-fee rate shapes supported on push. |
| Long-Term Disability | ✓ Synced | — filtered out | Same as STD. |
| Group Life (incl. AD&D) | ✓ Synced | — filtered out | Fixed-amount and salary-multiple variants both supported. Contributions hardcoded as Employer / Percentage. |
| Voluntary Life | — not mapped | — not mapped | EN exposes; Plansight has Voluntary Life elsewhere but no EN mapping. |
| Accident · Cancer · Critical Illness · Hospital Indemnity | — not mapped | — filtered out | Voluntary lines absent from QuoteNotification. |
| HRA · HSA · FSA · COBRA · EAP | — not mapped | — filtered out | Admin/non-insurance products absent from EN integration. |
Sources: QuoteNotification.php:85-108 (push type map), Controller.php:494-497 + line 424 (pull filter — hardcoded useTypes = ['Medical']). See the interactive Sync Map for field-level coverage.
For each benefit type we push to EN, here's exactly which Plansight column lands in which EN field path — and the fields EN's schema accepts that we don't currently send. Plain table, three columns: what we have, where it goes, and whether it's mapped.
integrations/EmployeeNavigator/docs/Quoting Engine QuoteCensusApi v1 08 21 24.json). Authoritative.QuoteNotification.php or a plantype attributePath in config/plantypeFiles/. The dev who built the integration had access to the broader EN docs at the time. Trustworthy but second-hand.All three health-shaped lines share the same EN schema and the same Plansight push code path. The mappings below apply identically to Medical, Dental, and Vision — the only differences are which Plansight quote rows are populated for each line, not how the fields map.
| Plansight field | EN field path | Status | Source | Notes | ||||
|---|---|---|---|---|---|---|---|---|
| Configuration | ||||||||
QuoteBenefitHistory.inForcePolicyNumber | plan.configuration.policyNumber | Mapped | Code | QuoteNotification.php:115 | QuoteBenefitHistory.inForcePolicyNumber | plan.configuration.policyNumber | Mapped | QuoteNotification.php:115 |
Carrier.name / employeeNavigatorName | plan.configuration.carrierName | Mapped | Code | Lines 116, 135 — uses employeeNavigatorName when populated | Carrier.name / employeeNavigatorName | plan.configuration.carrierName | Mapped | Lines 116, 135 — uses employeeNavigatorName when populated |
QuoteBenefitHistory.name | plan.configuration.planName | Mapped | Spec | Line 117 | QuoteBenefitHistory.name | plan.configuration.planName | Mapped | Line 117 |
QuoteBenefitHistory.inForceStartDate | plan.configuration.startDate | Mapped | Code | Line 118 — UTC ISO 8601 | QuoteBenefitHistory.inForceStartDate | plan.configuration.startDate | Mapped | Line 118 — UTC ISO 8601 |
QuoteBenefitHistory.inForceEndDate | plan.configuration.endDate + nextRenewalDate | Mapped | Code | Lines 119–120 — same value to both | QuoteBenefitHistory.inForceEndDate | plan.configuration.endDate + nextRenewalDate | Mapped | Lines 119–120 — same value to both |
QuoteBenefitHistory.fundingType | plan.configuration.isSelfFunded | Mapped | Code | Line 122 — true when == 'Self Funded' | QuoteBenefitHistory.fundingType | plan.configuration.isSelfFunded | Mapped | Line 122 — true when == 'Self Funded' |
Rfp.serviceState or Quote.state | plan.configuration.situsState | Conditional | Spec | Lines 142–161 — uses RFP value if originalRfpId set, else falls back | Rfp.serviceState or Quote.state | plan.configuration.situsState | Conditional | Lines 142–161 — uses RFP value if originalRfpId set, else falls back |
| Plan design (services) | ||||||||
| Plansight quote design fields (per Plantypes config) | plan.planCommunications.benefitSummary.services[].type | Mapped | Spec | Lines 183–242 — driven by field.integrations.employeeNavigator config | Plansight quote design fields (per Plantypes config) | plan.planCommunications.benefitSummary.services[].type | Mapped | Lines 183–242 — driven by field.integrations.employeeNavigator config |
| Plansight design value (in-network) | plan.planCommunications.benefitSummary.services[].inNetworkValue | Mapped | Code | Lines 213–224 | Plansight design value (in-network) | plan.planCommunications.benefitSummary.services[].inNetworkValue | Mapped | Lines 213–224 |
| Plansight design value (preferred network) | plan.planCommunications.benefitSummary.services[].preferredNetworkValue | Conditional | Code | Only sent when inNetworkOptions == 2 | Plansight design value (preferred network) | plan.planCommunications.benefitSummary.services[].preferredNetworkValue | Conditional | Only sent when inNetworkOptions == 2 |
| Plansight design value (out-of-network) | plan.planCommunications.benefitSummary.services[].outOfNetworkValue | Mapped | Code | Lines 227–230 | Plansight design value (out-of-network) | plan.planCommunications.benefitSummary.services[].outOfNetworkValue | Mapped | Lines 227–230 |
| — | plan.benefits[].employee.flatBenefit.amount | Not mapped | Spec | EN supports flat-benefit copays / deductibles. Plansight doesn't send | — | plan.benefits[].employee.flatBenefit.amount | Not mapped | EN supports flat-benefit copays / deductibles. Plansight doesn't send |
| — | plan.benefits[].dependents.incrementsBenefit | Not mapped | Spec | EN supports dependent-specific cost-share structures | — | plan.benefits[].dependents.incrementsBenefit | Not mapped | EN supports dependent-specific cost-share structures |
| Rates — composite (non-ACA) | ||||||||
QuoteBenefitHistory.rateEmployeeOnly | plan.rates.compositeRates.tierRates[Employee].rate | Conditional | Spec | Line 625 — sent when aca != 1 | QuoteBenefitHistory.rateEmployeeOnly | plan.rates.compositeRates.tierRates[Employee].rate | Conditional | Line 625 — sent when aca != 1 |
rateEmployeeSpouse | ...tierRates[EmployeePlusSpouse].rate | Conditional | Spec | Line 630 | rateEmployeeSpouse | ...tierRates[EmployeePlusSpouse].rate | Conditional | Line 630 |
rateEmployeeChild | ...tierRates[EmployeePlusChild].rate | Conditional | Spec | Line 635 | rateEmployeeChild | ...tierRates[EmployeePlusChild].rate | Conditional | Line 635 |
rateEmployeeChildren | ...tierRates[EmployeePlusChildren].rate | Conditional | Spec | Line 640 | rateEmployeeChildren | ...tierRates[EmployeePlusChildren].rate | Conditional | Line 640 |
rateEmployeeFamily | ...tierRates[EmployeePlusFamily].rate | Conditional | Spec | Line 645 | rateEmployeeFamily | ...tierRates[EmployeePlusFamily].rate | Conditional | Line 645 |
| — | ...tierRates[EmployeePlusDomesticPartner].rate | Not mapped | Spec | EN supports a domestic-partner tier; Plansight schema has no equivalent column | — | ...tierRates[EmployeePlusDomesticPartner].rate | Not mapped | EN supports a domestic-partner tier; Plansight schema has no equivalent column |
| Rates — ACA age-banded + tobacco | ||||||||
QuoteACARate.rates[Y##] | plan.rates.acaAgeBasedRates.rates[].rate | Conditional | Code | Lines 611–616 — sent when aca == 1 | QuoteACARate.rates[Y##] | plan.rates.acaAgeBasedRates.rates[].rate | Conditional | Lines 611–616 — sent when aca == 1 |
| Age extracted from key | ...rates[].fromAge / toAge | Conditional | Code | Same age in both fields per ACA convention | Age extracted from key | ...rates[].fromAge / toAge | Conditional | Same age in both fields per ACA convention |
QuoteACARate.rates[Y##] | ...tobaccoBasedRates[].rateNonsmoker | Conditional | Code | Lines 596, 600 — when tobacco rates exist on the rating area | QuoteACARate.rates[Y##] | ...tobaccoBasedRates[].rateNonsmoker | Conditional | Lines 596, 600 — when tobacco rates exist on the rating area |
QuoteACARate.tobaccoRates[Y##] | ...tobaccoBasedRates[].rateSmoker | Conditional | Code | Lines 593, 601 | QuoteACARate.tobaccoRates[Y##] | ...tobaccoBasedRates[].rateSmoker | Conditional | Lines 593, 601 |
| Contributions | ||||||||
| — | plan.contributions.contributionGroupings[].monthlyContributions[] | Not mapped | Code | EN accepts premium-sharing data on health plans; Plansight doesn't populate (only Group Life does — see below) | — | plan.contributions.contributionGroupings[].monthlyContributions[] | Not mapped | EN accepts premium-sharing data on health plans; Plansight doesn't populate (only Group Life does — see below) |
Pulled from config/plantypeFiles/{medical,dental,vision}.php. These are the design fields the broker fills in on Plansight that get pushed into plan.planCommunications.benefitSummary.services[] on the EN side — each row shows the Plansight key/label and the EN service-type identifier it lands as.
| Plansight field | EN service type |
|---|---|
| Plan Design - Medical Deductible | |
Per IndividualDeductibleIndividual | DeductibleIndividual |
Per FamilyDeductibleFamily | DeductibleFamily |
Deductible TypeDeductibleType | DeductibleType |
Deductible | OOP AccumulationDeductibleOopAccumulation | CalendarOrPlanYearDeductible |
| Plan Design - Out-of-pocket Maximum | |
Per IndividualOOPIndividual | OutOfPocketMaximumIndividual |
Per FamilyOOPFamily | OutOfPocketMaximumFamily |
| Plan Design - Office Visit | |
Primary PhysicianPrimaryCareDoctor | PrimaryCarePhysician |
| Specialist | Specialist |
Specialist ReferralSpecialistReferral | SpecialistReferralRequired |
| Telehealth | VirtualTelemedicineVisit |
| Plan Design - Tests | |
Preventive ServicesPreventativeServices | PreventiveServices |
Diagnostic Test (X-Ray, Blood Work)Diagnostic | DiagnosticTest |
Imaging (CT/PET Scans, MRIs)Imaging | Imaging |
| Plan Design - Prescription Drugs | |
Per IndividualRxIndividualDeductible | DeductiblePrescription |
Tier 1RxTier1 | GenericDrugsTier1 |
Tier 3RxTier3 | PreferredBrandDrugsTier2 |
Tier 4RxTier4 | NonPreferredBrandDrugsTier3 |
Tier 5RxTier5 | SpecialtyDrugsTier4 |
Mail OrderRxMailOrder | MailOrderRx |
| Plan Design - Outpatient Surgery | |
Facility FeeOutpatientFacility | HospitalizationOutpatient |
Free Standing FacilityOutpatientFreeStandingFacility | HospitalizationOutpatient |
Physician / Surgeon FeeOutpatientPhysician | SurgeryOutpatient |
| Plan Design - Immediate Attention | |
Emergency Room CopayEmergencyRoomCopay | EmergencyRoom |
Urgent CareUrgentCare | UrgentCare |
| Plan Design - Inpatient Services | |
Facility FeeHospitalFacility | HospitalizationInpatient + Coinsurance |
Physician / Surgeon FeeHospitalPhysician | SurgeryInpatient |
| Plan Design - Substance Abuse | |
Outpatient ServicesOutpatientMental | MentalHealthOutpatient |
Inpatient ServicesInpatientMental | MentalHealthInpatient |
| Plan Design - Maternity / Pregnancy | |
Office VisitsMaternityOffice | MaternityVisits |
| Plansight field | EN service type |
|---|---|
| Schedule Of Benefits | |
Individual DeductibleDeductibleIndividual | DeductibleIndividual |
Family DeductibleDeductibleFamily | DeductibleFamily |
Deductible AccumulationDeductibleAccumulation | BenefitPeriod |
Annual MaximumAnnualMaxIndividual | AnnualMaximumBenefit |
Preventive (I)BenefitSchedPreventiveI | ??? |
Preventive (I)BenefitSchedPreventiveI | OralExaminations |
Basic (II)BenefitSchedBasicII | Fillings |
| Dental Services | |
| Exams | OralExaminations |
| Cleanings | CleaningsAdult + CleaningsChildren |
Basic FillingBasicFilling | Fillings |
| Periodontics | Periodontal |
| Crowns | Crowns |
| Dentures | CompleteDenture |
| Implants | ImplantCoverage |
| Dental Waiting Periods | |
Preventive (I)WaitingPeriodPreventiveI | WaitingPeriodPreventativeServices |
Basic (II)WaitingPeriodBasicII | WaitingPeriodBasicServices |
Major (III)WaitingPeriodMajorIII | WaitingPeriodMajorServices |
| Orthodontics | |
Orthodontics (IV)Orthodontics | OrthodontiaServices |
Lifetime Ortho. Max.LifetimeOrthoMax | OrthodontiaLifetimeMaximum |
Orthodontics AgeOrthodonticsAge | OrthodontiaEligibility |
Ortho (IV) Wait PeriodOrthodonticsIVWaitPeriod | WaitingPeriodOrthodontiaServices |
| Plansight field | EN service type |
|---|---|
| Benefit Frequency | |
Eye ExamExamFrequency | EyeExams |
FramesFramesFrequency | Frames |
LensesLensesFrequency | Lenses |
Contacts (in lieu of glasses)ContactsFrequency | ContactLenses |
| Plan Provisions | |
Eye ExamEyeExams | ExamCopay |
Single Vision LensesSingleVisionLenses | SingleVisionLenses |
Bifocal LensesBifocalLenses | BifocalLenses |
Trifocal LensesTrifocalLenses | TrifocalLenses |
Basic Progressive LensesBasicProvisionLenses | ProgressiveLenses |
Premium Progressive LensesPremiumProgressiveLenses | PremiumProgressiveLensOptions |
Contacts Allowance - ElectiveContactsElective | ContactsAllowance |
Contacts Visually NecessaryContactsNecessary | ContactsAllowanceMedicallyNecessary |
| Frames | |
Coverage AllowanceFramesCovered | FrameAllowance |
| Corrective Vision Services | |
Lasik Vision CorrectionLasikDiscount | LaserEyeCorrection |
Group Life uses a different shape — a benefit-amount calculation (flat or salary-multiple) plus an explicit contributions block. Plan Type itself can't be set via the API in v3.0 (it's a manual selection in EN); Plansight sends a descriptive label only.
| Plansight field | EN field path | Status | Source | Notes | ||||
|---|---|---|---|---|---|---|---|---|
| Configuration | ||||||||
| (same Configuration block as Health) | plan.configuration.{policyNumber, carrierName, planName, startDate, endDate, isSelfFunded, situsState} | Mapped | Spec | Identical to Health benefits — see above | (same Configuration block as Health) | plan.configuration.{policyNumber, carrierName, planName, startDate, endDate, isSelfFunded, situsState} | Mapped | Identical to Health benefits — see above |
QuoteBenefitHistory.groupLifeCoverage | plan.configuration.carrierPlanTypeDescription | Mapped | Code | Lines 514–525 — sent as descriptive text; EN's API can't set Plan Type itself in v3.0 (manual in EN UI) | QuoteBenefitHistory.groupLifeCoverage | plan.configuration.carrierPlanTypeDescription | Mapped | Lines 514–525 — sent as descriptive text; EN's API can't set Plan Type itself in v3.0 (manual in EN UI) |
| Benefits (employee) | ||||||||
QuoteBenefitHistory.deathBenefit-X (when groupLifeCalculation == 'fixed') | plan.benefits[].employee.flatBenefit.amount | Conditional | Spec | Lines 383–415 | QuoteBenefitHistory.deathBenefit-X (when groupLifeCalculation == 'fixed') | plan.benefits[].employee.flatBenefit.amount | Conditional | Lines 383–415 |
deathBenefit-X (when groupLifeCalculation == 'salary') | plan.benefits[].employee.multipleOfEarningsBenefit.multipleOfEarnings | Conditional | Spec | Lines 406–414 | deathBenefit-X (when groupLifeCalculation == 'salary') | plan.benefits[].employee.multipleOfEarningsBenefit.multipleOfEarnings | Conditional | Lines 406–414 |
maximumBenefit | ...multipleOfEarningsBenefit.maximumBenefit | Conditional | Code | Lines 410–411 — salary-multiple mode only | maximumBenefit | ...multipleOfEarningsBenefit.maximumBenefit | Conditional | Lines 410–411 — salary-multiple mode only |
| (combined Life + ADD when basic+ADD) | deathBenefit (combined into employee benefit) | Mapped | Code | Lines 283–285 — Life and ADD values combined into a single deathBenefit | (combined Life + ADD when basic+ADD) | deathBenefit (combined into employee benefit) | Mapped | Lines 283–285 — Life and ADD values combined into a single deathBenefit |
| Benefits (dependents) | ||||||||
| — | plan.benefits[].dependents.approvedBenefitAmount | Not mapped | Spec | EN supports per-dependent benefit amount | — | plan.benefits[].dependents.approvedBenefitAmount | Not mapped | EN supports per-dependent benefit amount |
| — | plan.benefits[].dependents.incrementsBenefit | Not mapped | Spec | EN supports spouse/child benefit increments | — | plan.benefits[].dependents.incrementsBenefit | Not mapped | EN supports spouse/child benefit increments |
| Contributions | ||||||||
hardcoded "Percentage" | ...monthlyContributions[].contributionType | Mapped | Code | Line 448 | hardcoded "Percentage" | ...monthlyContributions[].contributionType | Mapped | Line 448 |
hardcoded "Employer" | ...monthlyContributions[].contributionBasis | Mapped | Code | Line 449 | hardcoded "Employer" | ...monthlyContributions[].contributionBasis | Mapped | Line 449 |
groupLifePremiumPaidBy | ...monthlyContributions[memberType=Dependents].contribution | Mapped | Code | Lines 453–463 — set to 0 or 100 (percentage) | groupLifePremiumPaidBy | ...monthlyContributions[memberType=Dependents].contribution | Mapped | Lines 453–463 — set to 0 or 100 (percentage) |
contributionVoluntary | ...monthlyContributions[memberType=Employee].contribution | Mapped | Spec | Lines 465–479 — set to 0 or 100 (percentage) | contributionVoluntary | ...monthlyContributions[memberType=Employee].contribution | Mapped | Lines 465–479 — set to 0 or 100 (percentage) |
| — | ...contributionGroupings[].memberContributions | Not mapped | Code | EN supports per-member contribution rules; Plansight only sends type-level | — | ...contributionGroupings[].memberContributions | Not mapped | EN supports per-member contribution rules; Plansight only sends type-level |
| Rates | ||||||||
| — | plan.rates.compositeRates.tierRates[] | Not mapped | Code | EN accepts composite life rates; Plansight doesn't send (optional in EN) | — | plan.rates.compositeRates.tierRates[] | Not mapped | EN accepts composite life rates; Plansight doesn't send (optional in EN) |
Pulled from config/plantypeFiles/groupLife.php — Plansight design fields with integrations.employeeNavigator.attributePath set, and the EN payload paths they land in.
| Plansight field | EN field path |
|---|---|
| Other | |
| groupLifeCalculation-X | fixed: benefits.employee.flatBenefit · salary: benefits.employee.multipleOfEarningsBenefit |
Death BenefitdeathBenefit-X | fixed: benefits.employee.flatBenefit.amount · salary: benefits.employee.multipleOfEarningsBenefit.multipleOfEarnings |
Maximum BenefitmaximumBenefit-X | benefits.employee.multipleOfEarningsBenefit.maximumBenefit |
| Dependent Benefits | |
Spouse Benefit AmountspouseBenefit | benefits.dependents.incrementsBenefit.spouseIncrement |
Child(ren) Benefit AmountchildBenefit | benefits.dependents.incrementsBenefit.childIncrement |
| Rates | |
Life Rate /per $1,000Life | rates.compositeRate.rate |
Spouse Rate /per UnitSPOUSE | benefits.dependents.incrementsBenefit.monthlyPremiumOption.combinedPremium.dependentMonthlyPremium |
Child Rate /per UnitCHILD | benefits.dependents.incrementsBenefit.monthlyPremiumOption.combinedPremium.dependentMonthlyPremium |
STD supports both Insured and ASO funding via the benefitType field, which overrides the global isSelfFunded. Rate structure is selectable: Composite, Age-Banded, or ASO Flat-Fee. Plansight sends the right rate shape based on rateStructure.
| Plansight field | EN field path | Status | Source | Notes | ||||
|---|---|---|---|---|---|---|---|---|
| Configuration | ||||||||
| (same Configuration block as Health) | plan.configuration.{policyNumber, carrierName, planName, startDate, endDate, situsState} | Mapped | Spec | Identical to Health benefits | (same Configuration block as Health) | plan.configuration.{policyNumber, carrierName, planName, startDate, endDate, situsState} | Mapped | Identical to Health benefits |
QuoteBenefitHistory.<benefitType> (Insured / ASO) | plan.configuration.isSelfFunded | Mapped | Spec | Lines 305–327 — overrides the global funding flag for STD | QuoteBenefitHistory.<benefitType> (Insured / ASO) | plan.configuration.isSelfFunded | Mapped | Lines 305–327 — overrides the global funding flag for STD |
| Benefit design | ||||||||
| — | plan.benefits[].employee.approvedBenefitAmount | Not mapped | Spec | EN supports active benefit cap amounts | — | plan.benefits[].employee.approvedBenefitAmount | Not mapped | EN supports active benefit cap amounts |
| — | plan.benefits[].employee.benefitCalculationMethod | Not mapped | Spec | EN schema allows; Plansight has the data but doesn't send it | — | plan.benefits[].employee.benefitCalculationMethod | Not mapped | EN schema allows; Plansight has the data but doesn't send it |
| — | plan.benefits[].employee.eliminationPeriod | Not mapped | Spec | EN supports waiting periods before benefit kicks in | — | plan.benefits[].employee.eliminationPeriod | Not mapped | EN supports waiting periods before benefit kicks in |
| — | plan.benefits[].employee.integrationWithOtherIncome | Not mapped | Spec | EN supports coordination with other income sources | — | plan.benefits[].employee.integrationWithOtherIncome | Not mapped | EN supports coordination with other income sources |
| Rates | ||||||||
Y19_Under (when rateStructure == 'Composite') | plan.rates.compositeRate.rate | Conditional | Code | Lines 344–349 | Y19_Under (when rateStructure == 'Composite') | plan.rates.compositeRate.rate | Conditional | Lines 344–349 |
Y20_24 … Y70_Above (when rateStructure == 'Age Banded') | plan.rates.ageBasedRates.rates[].fromAge / toAge / rate | Conditional | Code | Lines 350–372 — age boundaries extracted from key names | Y20_24 … Y70_Above (when rateStructure == 'Age Banded') | plan.rates.ageBasedRates.rates[].fromAge / toAge / rate | Conditional | Lines 350–372 — age boundaries extracted from key names |
Y19_Under (when rateStructure == 'ASO') | plan.rates.asoFlatRate.flatMonthlyFee | Conditional | Code | Lines 334–340 — STD-only path; LTD has no ASO support | Y19_Under (when rateStructure == 'ASO') | plan.rates.asoFlatRate.flatMonthlyFee | Conditional | Lines 334–340 — STD-only path; LTD has no ASO support |
| — | plan.rates.ageBasedRates.rates[].rateAdjustmentFactor | Not mapped | Code | EN supports rate modifiers | — | plan.rates.ageBasedRates.rates[].rateAdjustmentFactor | Not mapped | EN supports rate modifiers |
Pulled from config/plantypeFiles/stDisability.php.
| Plansight field | EN field path |
|---|---|
| Schedule of Benefits | |
Injury Elimination PeriodinjuryEliminationPeriod | benefits.details.accidentWaitingPeriodDays |
Sickness Elimination PeriodsicknessEliminationPeriod | benefits.details.sicknessWaitingPeriodDays |
Minimum Weekly BenefitminimumWeeklyBenefit | benefits.percentEarningsBenefit.minimumBenefit |
Maximum Benefit DurationmaximumBenefitDuration | benefits.details.benefitDurationWeeks |
| Other | |
Benefit PercentagebenefitPercentage-X | benefits.percentEarningsBenefit.benefitPercentEarnings |
Maximum Weekly BenefitmaxWeeklyBenefit-X | benefits.percentEarningsBenefit.maximumBenefit |
| Rates | |
Rate TypebenefitType | aso: configuration.isSelfFunded |
Rate StructurerateStructure | composite: rates.compositeRate.rate · ageBanded: rates.ageBasedRates.rates · asoFlatMonthlyFee: rates.asoFlatRate.flatMonthlyFee |
LTD is structurally identical to STD with two exceptions: (1) no ASO mode — Plansight always sends LTD as Insured (line 315 forces this); (2) the Maximum Benefit Duration field is uniquely relevant to LTD and isn't mapped on either side.
| Plansight field | EN field path | Status | Source | Notes | ||||
|---|---|---|---|---|---|---|---|---|
| Differences from STD | ||||||||
| (no ASO equivalent) | plan.rates.asoFlatRate.flatMonthlyFee | Not supported | Spec | Line 315 — $asoSelected = true only when insuranceType != 'ltDisability'. LTD always pushed as Insured | (no ASO equivalent) | plan.rates.asoFlatRate.flatMonthlyFee | Not supported | Line 315 — $asoSelected = true only when insuranceType != 'ltDisability'. LTD always pushed as Insured |
| — | plan.benefits[].employee.maximumBenefitDuration | Not mapped | Spec | EN supports max benefit period (months / years) — LTD-specific concept | — | plan.benefits[].employee.maximumBenefitDuration | Not mapped | EN supports max benefit period (months / years) — LTD-specific concept |
| All Configuration, Benefit Design, and non-ASO Rate fields are identical to STD above. | ||||||||
Pulled from config/plantypeFiles/ltDisability.php.
| Plansight field | EN field path |
|---|---|
| Schedule of Benefits | |
Elimination PeriodeliminationPeriod | benefits.details.waitingPeriodDays |
Benefit DurationbenefitDuration | benefits.details.benefitDuration |
| Other | |
Benefit SchedulebenefitSchedule-X | benefits.percentEarningsBenefit.benefitPercentEarnings |
Maximum Monthly BenefitmaximumMonthlyBenefit-X | benefits.percentEarningsBenefit.maximumBenefit |
Minimum Monthly BenefitminimumMonthlyBenefit-X | benefits.percentEarningsBenefit.minimumBenefit |
| Rates | |
Rate TypebenefitType | aso: configuration.isSelfFunded |
Rate StructurerateStructure | composite: rates.compositeRate.rate · ageBanded: rates.ageBasedRates.rates |
plan.benefits[].dependents.*. Plansight sends none of it. Quotes are plan-level, not member-enrolled-benefit level.flatBenefit.amount, multipleOfEarningsBenefit) — common for HSA-funded plans, employer-paid premium portions, etc. Today only Group Life uses this shape.plan.contributions.contributionGroupings[] on every benefit type. Plansight populates it only for Group Life. For Medical / Dental / Vision / Disability we know who pays what — we just don't tell EN.EmployeePlusDomesticPartner tier rate on health benefits. Plansight has no quote-side column for it, so even if we wanted to send it, we'd need a schema change first.RFP.serviceState when an originalRfpId is present, falls back to quote.state. If neither is populated, the field is omitted — risk of EN-side validation failure on edge cases worth a defensive check.QuoteNotification.php:501-511. Plansight sends Plan Type as a description string only — broker still sets the actual Plan Type in the EN UI manually.Eight gaps came out of comparing the EN OpenAPI spec (Quote Census v1, 2024-08-21) against integrations/EmployeeNavigator/ in Plansight APP commit 9d1afd54d5df. Each is a fixable change rather than a structural rewrite.
Problem. EN exposes a per-dependent tobaccoUseInformation.useTobacco field. Controller.php:486 assigns the parent employee's tobacco value to every dependent instead of reading each dependent's own value.
Fix. Read employees[].dependents[].demographics.tobaccoUseInformation.useTobacco per dependent. Spouse-tobacco rates differ from employee-tobacco rates for many carriers, so this matters.
In integrations/EmployeeNavigator/Controller.php around line 486, replace the dependent tobacco assignment so it reads $dependent['demographics']['tobaccoUseInformation']['useTobacco'] for each dependent rather than copying from the parent employee. Confirm by adding a fixture where parent and spouse have different tobacco values and verifying the census record reflects the dependent's own value.
Problem. Controller.php:424 hardcodes $useTypes = ['Medical']. Even though we successfully push Dental, Vision, STD, LTD, and Group Life plans to EN, when we pull census back, we silently drop every enrollment for those same lines. Brokers see employees from EN but not their dental/vision/life/disability elections.
Fix. Expand $useTypes to match the push side (Medical, Dental, Vision, STD, LTD, Group Life). The benefit-type loop and plan-storage logic in lines 494-549 should already handle additional types — verify by tracing each enrollment branch.
In Controller.php:424, change the hardcoded useTypes array from ['Medical'] to the full set we push: ['Medical', 'Dental', 'Vision', 'ShortTermDisability', 'LongTermDisability', 'Life']. Confirm that the downstream code at lines 494-549 stores enrollments per benefit type without conflating them, and that the existing tests in tests/Feature/EmployeeNavigator/Operations/ still pass.
Problem. EN exposes employees[].companyStructureMapping with eligibilityClass, division, department, businessUnit, and officeLocation. None of these are read by Plansight. For a multi-class employer (different contributions / waiting periods / plan eligibility per class), this means we can't honor the broker's class-based logic.
Fix (medium lift). Capture eligibilityClass on the census record at minimum. Treat it as a typed enum — surface in the RFP UI and let the broker map EN classes to Plansight rate classes. Division/department can be string fields for display.
Problem. EN sends employmentInformation.benefitHireDate, jobTitle, benefitWorkState, and employmentCompensation.annualBenefitSalary. None are read in the census import.
What we'd gain. Hire date drives waiting-period logic and ACA eligibility — without it, brokers manually enter dates we already have. Salary enables salary-based plan modeling (multiple-of-earnings life, salary-banded disability). Work state matters for situs-state rating on multi-state employers.
Problem. EN sends benefitTier per enrollment (EmployeePlusFamily, EmployeePlusSpouse, etc.). Plansight ignores it and re-derives tier from dependent count. For most cases the answer matches, but EN's value is the source of truth for what the employee actually elected — re-deriving can disagree (e.g., if an employee has dependents but waived spouse coverage).
Fix. Store EN's benefitTier as the authoritative tier on the census enrollment. Use Plansight-derived tier as a sanity check, log when they differ.
Problem. EN sends employeeBenefit.effectiveDate + terminationDate (and dependent equivalents). Plansight stores neither. For renewal pulls or mid-year changes, we can't tell from the census whether someone is actively enrolled or aged off.
Fix. Capture both dates on the census enrollment. Treat enrollments where terminationDate < today as inactive.
Problem. Plansight quotes voluntary product lines through Plan Facts, but the EN integration's QuoteNotification only maps Medical/Dental/Vision/STD/LTD/Life. Voluntary lines never reach EN — the broker has to re-key them.
Fix (larger). Add benefit-type branches in QuoteNotification.php:85-108 for the voluntary lines. Each needs its own service-attribute map and rate shape. EN's API supports them — Plansight's mapping doesn't yet.
Problem. The OpenAPI spec we audit against (Quoting Engine QuoteCensusApi v1 08 21 24.json) is from 2024-08-21. The Plansight code references API v1.0 for census (QuoteCensus.php:63) and v3.0 for notifications (QuoteNotification.php:700). No version negotiation. EN may have published changes since.
Also: the example census response includes benefitEligibility.isEligible, period, and decision fields that aren't in the OpenAPI schema. Plansight reads decision == 'Elected'. If EN tightens the schema, we'd silently break.
Fix. Confirm with EN that v1 census + v3 notifications are still current and that the example-response fields are formally part of the schema. Pin the spec we ship in integrations/EmployeeNavigator/docs/ to a known-good date and refresh on a schedule.
The idea (not built). Today, when a broker starts a renewal RFP in Plansight, they have to either upload current-plan PDFs (which Plan Facts then parses) or hand-key the existing plan design. If EN already holds the same employer's current plan data, we may be able to pull plans + plan-design + current rates from EN and pre-populate the "current plans" side of the renewal automatically — bypassing the PDF dance for employers that are already on EN.
What's documented today. The OpenAPI spec we have covers Quote Census only — employee + dependent + enrollment data, not plan design. EN's wider API surface likely includes plan endpoints (the OpenAPI health benefit / life benefit / supplemental benefit / disability benefit objects within the census imply richer plan-data shapes exist server-side). We need the broader EN partner API spec to confirm exactly what's reachable.
If feasible, what we'd gain. A "Connect EN → import current plans" button on renewal kickoff that eliminates 80% of the data-entry work for any employer already on EN. Better-than-Plan-Facts accuracy for those employers, since EN's plan data is the source of truth for what's actually being administered.
Ask EN partner support for the partner API spec covering plan/group/benefit administration (not just Quote Census). Confirm endpoints exist for: list plans by company, get plan-design detail, get current rates by tier. If yes, scope a new integrations/EmployeeNavigator/Operations/ImportCurrentPlans.php that mirrors the QuoteCensus token + auth pattern.
What changed. Plansight's dental plantype now has category-level summary fields under Schedule of Benefits: BenefitSchedPreventiveI (Type I), BenefitSchedBasicII (Type II), Type III (Major), and Type IV (Orthodontia). These are new — before, we only had the individual procedure-level fields (Exams, Cleanings, Basic Filling, Crowns, Dentures, Implants, Periodontics, Orthodontics, etc.), which already flow into EN's service-type fields (OralExaminations, CleaningsAdult/CleaningsChildren, Fillings, Crowns, etc.). Brokers were already filling those individual benefits in — they just rolled up implicitly under each category.
The gap. The new Type I–IV summary fields in config/plantypeFiles/dental.php aren't yet wired into the EN push the way the individual procedures are. Today their integrations.employeeNavigator config either points back at one of the same procedure-level EN attributes (e.g. BenefitSchedPreventiveI → OralExaminations) or is missing entirely (Type III, Type IV). That means the category-level value the broker enters at the top of the dental Schedule of Benefits doesn't actually land in EN as a Schedule-of-Benefits roll-up — it overwrites a single procedure on EN's side, or gets dropped.
What to do. Update the dental EN integration so each Type I–IV field pushes to the correct EN Schedule of Benefits roll-up rather than overwriting individual procedure values. Confirm the corresponding EN attribute names with the broader partner API spec (Edie's pending request) before wiring — we want the roll-ups to coexist cleanly with the individual procedure fields, not collide with them.
In config/plantypeFiles/dental.php, audit the integrations.employeeNavigator.attribute values on BenefitSchedPreventiveI, BenefitSchedBasicII, the missing Type III field (Major), and Type IV (Orthodontia) under Schedule Of Benefits. They should map to EN's category-level Schedule-of-Benefits attributes, not the individual procedure attributes already used by Exams, Cleanings, BasicFilling, Crowns, etc. (which live under the per-procedure Dental Services section). Verify EN attribute names with the partner API spec before wiring.
Most of the gaps are EN sends data, Plansight ignores it. That's the definition of a one-way valve — we built the integration to push our quote data out and pull the bare minimum to identify employees on the way back in. The pieces of EN data that would most help our quoting (class, salary, work state, hire date, coverage tier) are exactly the ones we drop.
Recommendation: tackle gaps #1 and #2 first — they're small, well-scoped, and unblock real broker pain (tobacco accuracy + showing all enrollments after a pull). Treat #3-#7 as a separate quarter since they require RFP / quote schema changes. #8 is a 30-minute conversation with EN. Gap #9 (current-plan import) is the big strategic prize — turns EN from a one-way push into a renewal accelerator. Gap #10 (dental Type I–IV roll-ups) is a small, well-scoped fix that pairs naturally with Edie's pending API spec.
Surface-level findings from a static read of the integration code. None block production; some are worth a quick verify next time the EN code is touched.
demographics.firstInitialLastNameUsed at Controller.php:454 for both employee and dependent. The name suggests "first initial + last name" but is mapped to a full name field downstream. Worth a quick verify against EN API docs.
Controller.php:454–455 truncates ZIP to 5 chars with a comment about "Vericred processing." Now that we're on IDEON, worth confirming this is still needed and updating the comment.
If a dependent's tobacco status is missing, Controller.php:465–486 copies the employee's value. Reasonable default, but may not reflect actual dependent tobacco use — worth flagging in the UI when this fallback fires.
Line 374 has 'onlyRedirect' => true which skips the actual QuoteCensus operation execution on callback (line 68). Suggests census pull's auth callback may not complete the census fetch in the same hop. Worth tracing once.
Unlike the IDEON integration which has exponential backoff, census and status queries have no retry logic. A transient EN failure shows the user an error rather than retrying. Low priority unless EN reliability becomes an issue.
Quote push errors aggregate but only surface title and errors from EN's ProblemDetails (Controller.php:292–301). Some EN-specific error contexts (e.g., field-level validation) may be obscured.
External documentation that explains the broker-side workflow. Use these alongside this audit page when onboarding new team members or troubleshooting customer questions.
Enabling and using the Employee Navigator integration
Plansight ↔ Employee Navigator plan data sync guide
Connect employers (across BenefitPoint and Employee Navigator)
Activate Employee Navigator Integration
Connect Employer to Employee Navigator
Push Renewal Plans to Employee Navigator
Push Plans from Plansight to Navigator
Employee Navigator Census Import