All benefits

Plan Facts Parsing Analysis · v1

Voluntary Life

Single-class parse covering employee voluntary life with age-banded rates, plus spouse and child dependent benefits, AD&D, age reductions, and waiver of premium.

Endpoint
POST /life_add
Parse method
parseQuoteVoluntaryLife()
Source range
PlanfactService.php:469–970 (~502 lines)
Source pull
plansight - Bit Bucket Pull 4.26

Bottom line

One concrete bug, three smaller items.

The parser is structurally fine, but a missing-dot path-string bug at line 805 causes the Spouse Benefit Amount lookup to silently fail (always returns null). Two other items are semantic naming mismatches worth a verify, and one is a feature gap (smoker / non-smoker rate variants). All four have Cursor prompts below.

Per-class handling

Single representative class — fallback chain picks "all" → "1" → first class.

Voluntary Life uses a single-class model rather than per-class. The parser finds one representative class via a fallback chain (lines 477–510): first class containing "all" (case-insensitive), or first class containing "1", or just the first class. All subsequent fields are keyed by that class name alone. Age bands are mapped to composite key names like voluntaryLifeLess19Employee, voluntaryLife2024Employee, etc.

What's mapped

Plan Facts fields → Quote fields. $allClass is the resolved class.

Plan Facts fieldQuote fieldNotes
Plan NamenameDirect (line 473)
Voluntary AD&D presencevoluntaryLifeADDIncluded / Not Included (line 512)
Voluntary Life.Employee Death Benefit.[Class].Benefit AmountvoluntaryLifeCalculationType + voluntaryLifePurchaseIncEmployee + voluntaryLifeMaxSalaryMultiplier$ prefix → Fixed Amount; else regex → Salary Based (line 519)
Voluntary Life.Employee Death Benefit.[Class].Benefit MaximumvoluntaryLifeMaxBenefitRegex extracts digits (line 536)
Voluntary Life.Employee Death Benefit.[Class].Guarantee Issue AmountvoluntaryLifeMaxGuaranteeIssueEmployeeRegex (line 547)
Voluntary Life.Employee Age Reductions.[Class] (up to 4)voluntaryLifeTier{1..4}Age + voluntaryLifeTier{1..4}Reduction + voluntaryLifeNumberOfTiers"By" type inverts (100 - amount); line 558
Voluntary Life.Waiver of Premium.[Class].Elimination PeriodvoluntaryLifeEliminationPeriodSwitch: 3/6/9/12 Mo., "Determined By Carrier" (line 596)
Voluntary Life.Waiver of Premium.[Class].DurationvoluntaryLifeDurationSwitch: 60/65 → priorTo60/65; SSNRA detected (line 630)
Voluntary Life.Accelerated Death Benefit.[Class].Benefit AmountvoluntaryLifeAcceleratedDeathBenefit + voluntaryLifePercentOfBenefitPresence + regex % (line 650)
Voluntary Life.Accelerated Death Benefit.[Class].Life ExpectancyvoluntaryLifeLifeExpectancySwitch: 6/12/24 (line 666)
Voluntary Life.Conversion.[Class].ConversionvoluntaryLifeConversionIncluded / Not Included (line 683)
Voluntary Life.Portability.[Class].PortabilityvoluntaryLifePortabilityIncluded / Not Included (line 698)
Voluntary Life.Rates.[Class].Employee Uni-smoker Rates (age-banded, 11 bands)voluntaryLifeLess19Employee, voluntaryLife2024Employee, … voluntaryLife75OverEmployeeComposite per band (line 713)
Voluntary AD&D.Rates.[Class].Employee Composite Rate.RatevoluntaryLifeVolAddEmployeeComposite (line 795)
Voluntary AD&D.[Service].[Class].Benefit Amount (6 services)voluntaryLifeSeatBelt, …AirBag, …Repatriation, …EducationBenefit, …ChildCare, …CommonCarrierIncluded / Not Included (line 768)
Voluntary Dependent Life.Spouse Death Benefit.[Class].Spouse Benefit AmountvoluntaryLifeSpouseBenefitsDescPath bug — see issue below (line 805)
Voluntary Dependent Life.Spouse Death Benefit.[Class].Spouse Benefit MaximumvoluntaryLifeMaxGuaranteeIssueSpouseRegex (line 812)
Voluntary Dependent Life.Spouse Death Benefit.[Class].Spouse Guarantee Issue AmountvoluntaryLifeGuaranteeIssueSpouseRegex (line 823)
Voluntary Dependent Life.Child Death Benefit.[Class].Child Benefit AmountvoluntaryLifeChildBenefitsDescDirect (line 834)
Voluntary Dependent Life.Child Death Benefit.[Class].Child Benefit MaximumvoluntaryLifeMaxGuaranteeIssueChild + voluntaryLifeDependentTier3Reduction⚠ Dual-assigns same value to a "Reduction" field (line 841)
Voluntary Dependent Life.Child Death Benefit.[Class].Child Benefit from Birth to 14 daysvoluntaryLifeNumberOfChildrenTiers + voluntaryLifeDependentTier1ReductionStored in "Reduction" field — likely misnamed (line 854)
Voluntary Dependent Life.Child Death Benefit.[Class].Child Benefit from 15 days to 6 monthsvoluntaryLifeDependentTier2ReductionStored in "Reduction" field — likely misnamed (line 868)
Voluntary Dependent Life.Child Death Benefit.[Class].Student Extension AgevoluntaryLifeFullTimeStudentRequired + voluntaryLifeStudentAgeRangeTo(line 882)
Voluntary Dependent Life.Waiver of Premium.[Class].Elimination PeriodvoluntaryLifeDependentLifeWaiverOfPremiumPresence flag only (line 890)
Voluntary Dependent Life.Portability.[Class].PortabilityvoluntaryLifeDependentLifePortabilityDefaults to Not Included (line 899)
Voluntary Dependent Life.Rates.[Class].Spouse Uni-smoker Rates (11 bands)voluntaryLifeLess19SpousevoluntaryLife75OverSpouseComposite per band (line 916)

Plan-level / not mapped

Plan Facts fields the parser doesn't read.

Potentially mapped incorrectly

Four items. The first is a confirmed-looking bug; the rest are semantic concerns worth a verify.

Missing dot in path string at line 805 PlanfactService.php:805

The path concatenation at line 805 is missing a dot before the last component, so the lookup never matches and $benefitAmount is always null. Spouse Benefit Amount silently fails to populate.

Cursor prompt
In app/Services/PlanfactService.php at line 805, inside parseQuoteVoluntaryLife(), there's a path-concatenation bug.

The current code is:
'Voluntary Dependent Life.Spouse Death Benefit.' . $allClass . 'Spouse Benefit Amount'

It should be:
'Voluntary Dependent Life.Spouse Death Benefit.' . $allClass . '.Spouse Benefit Amount'

(Missing dot before 'Spouse Benefit Amount'.)

This bug means the lookup never matches the Plan Facts response, and $benefitAmount ends up null. The voluntaryLifeSpouseBenefitsDesc Quote field is silently empty for every voluntary life parse.

Please:
1. Fix the path string.
2. Then grep this entire file for any other path concatenations like '$variable . \"...\"' (without a dot before the suffix) — there may be similar bugs elsewhere. Report any you find.
Child benefit amounts stored in fields named "Reduction" PlanfactService.php:841–880

Child Benefit Maximum and the age-stratified Child Benefit Amount values are written to voluntaryLifeDependentTier1Reduction, voluntaryLifeDependentTier2Reduction, and voluntaryLifeDependentTier3Reduction. The "Reduction" suffix implies a percentage reduction, but these store dollar amounts. Either the field names are misleading or the assignment is wrong.

Cursor prompt
In app/Services/PlanfactService.php inside parseQuoteVoluntaryLife() (around lines 841–880), Child Benefit Amount values are being assigned to fields named voluntaryLifeDependentTier{1..3}Reduction.

The "Reduction" suffix suggests a percentage reduction, but the values stored are dollar benefit amounts.

Please:
1. Find the form template that renders these Tier{1..3}Reduction fields. Confirm whether the form is treating them as benefit amounts (in which case the field name is misleading) or as reductions (in which case the assignment is wrong).
2. Show me the change you'd recommend — either rename the fields to voluntaryLifeChildTier{1..3}Benefit (or similar) end-to-end, OR fix the assignment to extract a reduction percentage rather than the dollar amount.
3. Don't change anything yet — show the diff and the impact.
Child Benefit Maximum dual-assigned PlanfactService.php:849–851

The Child Benefit Maximum value is assigned to BOTH voluntaryLifeMaxGuaranteeIssueChild AND voluntaryLifeDependentTier3Reduction. Two fields, same value. Likely a copy-paste residue.

Cursor prompt
In app/Services/PlanfactService.php at lines 849–851, inside parseQuoteVoluntaryLife(), the same Child Benefit Maximum value is being written to both voluntaryLifeMaxGuaranteeIssueChild AND voluntaryLifeDependentTier3Reduction.

Please:
1. Confirm the dual-assignment by showing me the lines.
2. Check the form templates that consume each field to determine which (or both) of those Quote fields actually need this value.
3. Show me a draft change that removes whichever assignment isn't needed. If both are needed, add a comment explaining why.
No smoker / non-smoker rate variant PlanfactService.php:714

The rate parsing is keyed to "Employee Uni-smoker Rates" only. If a carrier returns separate Smoker / Non-Smoker rate tables, the parser silently drops them. Worth confirming whether voluntary life Plan Facts responses ever include smoker splits.

Cursor prompt
In app/Services/PlanfactService.php at around line 714, inside parseQuoteVoluntaryLife(), the rate parsing reads:
$rates = $data['Voluntary Life']['Rates'][$allClass]['Employee Uni-smoker Rates'] ?? null;

This only handles uni-smoker (combined) rates. If Plan Facts ever returns separate "Smoker" and "Non-Smoker" rate tables for Voluntary Life, those would be dropped silently.

Please:
1. Search this codebase for any reference to "Non-Smoker" or "Tobacco" rate fields on the Quote model.
2. If smoker-specific rate fields exist on the Quote, draft a change that detects whether the Plan Facts response has uni-smoker OR split smoker/non-smoker, and parses accordingly.
3. If smoker-specific fields don't exist, recommend whether to add them or to log a warning when split rates are encountered.
4. Don't make changes yet — show the proposed approach.

Manual-entry-only fields

Recommendations

  1. Fix line 805 path bug 5 minutes

    Add the missing dot. Verify Spouse Benefit Amount populates after the fix.

  2. Resolve the Tier-Reduction naming question Hour

    Either rename the fields to …ChildTier{N}Benefit end-to-end, or fix the assignment to extract reduction percentages.

  3. Remove the dual-assignment at lines 849–851 15 minutes

    One field, one value.

  4. Confirm smoker-rate handling Quick check

    If carriers ever return split smoker/non-smoker rates, today's parse drops them. Check whether this matters in practice before adding code.

Code references

FileLinesPurpose
app/Services/PlanfactService.php304–316life_add endpoint config
app/Services/PlanfactService.php456–457Dispatch to parseQuoteVoluntaryLife
app/Services/PlanfactService.php469–970parseQuoteVoluntaryLife() — full parser
app/Services/PlanfactService.php477–510Class fallback chain (all → 1 → first)
app/Services/PlanfactService.php713–764Employee age-banded rates (11 bands)
app/Services/PlanfactService.php805Missing-dot path bug (Spouse Benefit Amount)
app/Services/PlanfactService.php841–880Child benefit / Tier-Reduction naming concerns
app/Services/PlanfactService.php916–967Spouse age-banded rates