Plan Facts Parsing Analysis · v1
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.
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.
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.
Plan Facts fields → Quote fields. $allClass is the resolved class.
| Plan Facts field | Quote field | Notes |
|---|---|---|
| Plan Name | name | Direct (line 473) |
| Voluntary AD&D presence | voluntaryLifeADD | Included / Not Included (line 512) |
| Voluntary Life.Employee Death Benefit.[Class].Benefit Amount | voluntaryLifeCalculationType + voluntaryLifePurchaseIncEmployee + voluntaryLifeMaxSalaryMultiplier | $ prefix → Fixed Amount; else regex → Salary Based (line 519) |
| Voluntary Life.Employee Death Benefit.[Class].Benefit Maximum | voluntaryLifeMaxBenefit | Regex extracts digits (line 536) |
| Voluntary Life.Employee Death Benefit.[Class].Guarantee Issue Amount | voluntaryLifeMaxGuaranteeIssueEmployee | Regex (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 Period | voluntaryLifeEliminationPeriod | Switch: 3/6/9/12 Mo., "Determined By Carrier" (line 596) |
| Voluntary Life.Waiver of Premium.[Class].Duration | voluntaryLifeDuration | Switch: 60/65 → priorTo60/65; SSNRA detected (line 630) |
| Voluntary Life.Accelerated Death Benefit.[Class].Benefit Amount | voluntaryLifeAcceleratedDeathBenefit + voluntaryLifePercentOfBenefit | Presence + regex % (line 650) |
| Voluntary Life.Accelerated Death Benefit.[Class].Life Expectancy | voluntaryLifeLifeExpectancy | Switch: 6/12/24 (line 666) |
| Voluntary Life.Conversion.[Class].Conversion | voluntaryLifeConversion | Included / Not Included (line 683) |
| Voluntary Life.Portability.[Class].Portability | voluntaryLifePortability | Included / Not Included (line 698) |
| Voluntary Life.Rates.[Class].Employee Uni-smoker Rates (age-banded, 11 bands) | voluntaryLifeLess19Employee, voluntaryLife2024Employee, … voluntaryLife75OverEmployee | Composite per band (line 713) |
| Voluntary AD&D.Rates.[Class].Employee Composite Rate.Rate | voluntaryLifeVolAddEmployee | Composite (line 795) |
| Voluntary AD&D.[Service].[Class].Benefit Amount (6 services) | voluntaryLifeSeatBelt, …AirBag, …Repatriation, …EducationBenefit, …ChildCare, …CommonCarrier | Included / Not Included (line 768) |
| Voluntary Dependent Life.Spouse Death Benefit.[Class].Spouse Benefit Amount | voluntaryLifeSpouseBenefitsDesc | ⚠ Path bug — see issue below (line 805) |
| Voluntary Dependent Life.Spouse Death Benefit.[Class].Spouse Benefit Maximum | voluntaryLifeMaxGuaranteeIssueSpouse | Regex (line 812) |
| Voluntary Dependent Life.Spouse Death Benefit.[Class].Spouse Guarantee Issue Amount | voluntaryLifeGuaranteeIssueSpouse | Regex (line 823) |
| Voluntary Dependent Life.Child Death Benefit.[Class].Child Benefit Amount | voluntaryLifeChildBenefitsDesc | Direct (line 834) |
| Voluntary Dependent Life.Child Death Benefit.[Class].Child Benefit Maximum | voluntaryLifeMaxGuaranteeIssueChild + voluntaryLifeDependentTier3Reduction | ⚠ Dual-assigns same value to a "Reduction" field (line 841) |
| Voluntary Dependent Life.Child Death Benefit.[Class].Child Benefit from Birth to 14 days | voluntaryLifeNumberOfChildrenTiers + voluntaryLifeDependentTier1Reduction | Stored in "Reduction" field — likely misnamed (line 854) |
| Voluntary Dependent Life.Child Death Benefit.[Class].Child Benefit from 15 days to 6 months | voluntaryLifeDependentTier2Reduction | Stored in "Reduction" field — likely misnamed (line 868) |
| Voluntary Dependent Life.Child Death Benefit.[Class].Student Extension Age | voluntaryLifeFullTimeStudentRequired + voluntaryLifeStudentAgeRangeTo | (line 882) |
| Voluntary Dependent Life.Waiver of Premium.[Class].Elimination Period | voluntaryLifeDependentLifeWaiverOfPremium | Presence flag only (line 890) |
| Voluntary Dependent Life.Portability.[Class].Portability | voluntaryLifeDependentLifePortability | Defaults to Not Included (line 899) |
| Voluntary Dependent Life.Rates.[Class].Spouse Uni-smoker Rates (11 bands) | voluntaryLifeLess19Spouse … voluntaryLife75OverSpouse | Composite per band (line 916) |
Plan Facts fields the parser doesn't read.
Four items. The first is a confirmed-looking bug; the rest are semantic concerns worth a verify.
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.
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 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.
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.
The Child Benefit Maximum value is assigned to BOTH voluntaryLifeMaxGuaranteeIssueChild AND voluntaryLifeDependentTier3Reduction. Two fields, same value. Likely a copy-paste residue.
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.
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.
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.
Add the missing dot. Verify Spouse Benefit Amount populates after the fix.
Either rename the fields to …ChildTier{N}Benefit end-to-end, or fix the assignment to extract reduction percentages.
One field, one value.
If carriers ever return split smoker/non-smoker rates, today's parse drops them. Check whether this matters in practice before adding code.
| File | Lines | Purpose |
|---|---|---|
| app/Services/PlanfactService.php | 304–316 | life_add endpoint config |
| app/Services/PlanfactService.php | 456–457 | Dispatch to parseQuoteVoluntaryLife |
| app/Services/PlanfactService.php | 469–970 | parseQuoteVoluntaryLife() — full parser |
| app/Services/PlanfactService.php | 477–510 | Class fallback chain (all → 1 → first) |
| app/Services/PlanfactService.php | 713–764 | Employee age-banded rates (11 bands) |
| app/Services/PlanfactService.php | 805 | Missing-dot path bug (Spouse Benefit Amount) |
| app/Services/PlanfactService.php | 841–880 | Child benefit / Tier-Reduction naming concerns |
| app/Services/PlanfactService.php | 916–967 | Spouse age-banded rates |