Plan Facts Parsing Analysis · v1
Multi-network parse (1–3 networks) with annual maximum, deductible, OOP, rollover account, service-level coinsurance (preventive / basic / major / orthodontic), waiting periods, and frequencies.
Several small fragility items, plus a dual-storage rollover.
The parser is comprehensive. Items below are minor: a brittle deductible-period regex (matches single "c"), a dual-stored rollover field with comments suggesting tech debt, a frequency regex with limited coverage, and hardcoded "Excess Responsibility" defaults.
Multi-network: detects 1, 2, or 3 networks; inNetworkOptions = 1 or 2 (line 2511). No employee-class concept. Rates by coverage tier (Employee Only / +Spouse / +Children / +Child / Family).
| Plan Facts field | Quote field | Notes |
|---|---|---|
| Plan Name | name | Direct (line 2508) |
| Networks | inNetwork1NetworkName, inNetwork2NetworkName, inNetworkOptions | Lines 2511–2548 |
| UCR (%) | outNetworkClaimPaymentBasis + Type | If numeric → percentage (line 2559) |
| Annual Maximum Amount.[Network] | inNetwork{1,2}AnnualMaxIndividual, outNetworkAnnualMaxIndividual | $0 → "Unlimited" (line 2592) |
| Deductible / Max (other copay services).[Network] | inNetwork{1,2}DeductibleIndividual etc. | via parseMoney |
| Deductible Period | inNetwork{1,2}DeductibleAccumulation, outNetworkDeductibleAccumulation | ⚠ Brittle regex (line 2645) |
| Annual Maximum Rollover.Rollover Amount | inNetwork{1,2}RolloverMaximum AND …RolloverVer2 + Type | ⚠ Dual-stored (line 2656) |
| Annual Maximum Rollover.Rollover Description | inNetwork{1,2}RolloverDescription | UI removed 2025-05-30; data preserved |
| Annual Maximum Rollover.Rollover Account Maximum | inNetwork{1,2}RolloverAccountLimit + Type | (line 2690) |
| Annual Maximum Rollover.Threshold Amount | inNetwork{1,2}RolloverThreshold + Type | (line 2707) |
| Preventive / Basic / Major / Orthodontic Services.Coinsurance.[Network] | inNetwork{1,2}Exams, …BenefitSchedPreventiveI, etc. | via parseCopay (line 2810) |
| Service.Deductible Applies | inNetwork{1,2}[Service]Ded | "yes" → AD; "no" → ND (line 2779) |
| Orthodontic Services.Child Eligibility Age / Adult Eligibility | inNetwork{1,2}OrthodonticsAge, inNetwork{1,2}OrthodonticsAdult, outNetworkOrthodonticsAge | (line 2841) |
| Waiting Period.Late Entrants (Ortho / Preventive / Basic / Major) | inNetwork{1,2}{Wait fields} (legacy + new "Months"/"None" enum) | Two formats stored; lines 2875–2986 |
| Frequencies.Cleanings | inNetwork{1,2}PreventiveFrequency | ⚠ Limited regex (line 2997) |
| Rates.[Coverage Tier] | rateEmployeeOnly, rateEmployeeSpouse, rateEmployeeChildren, rateEmployeeChild, rateEmployeeFamily | Line 3039 |
The Deductible Period switch maps "c", "calendar year", "ccalendar year" (typo), and "plan year" / "benefit year". The single-character "c" match is too permissive — any string starting with "c" or having a stray "c" might trigger Calendar Year incorrectly.
In app/Services/PlanfactService.php at around line 2645, inside parseQuoteDental(), the Deductible Period switch handles unusual values like a single "c" or "ccalendar year". The single-character "c" match is too permissive. Please: 1. Show me the switch. 2. Replace the single-character "c" case with explicit case-insensitive matching for "calendar year" only. 3. Add a default branch that logs a warning when an unexpected value is encountered, instead of silently mapping unknown strings. 4. Show me the proposed change.
Both inNetwork{1,2}RolloverMaximum (legacy) and inNetwork{1,2}RolloverVer2 + Type (new) are written for every parse. Comments suggest the old format was kept "in case we want to revert." Worth deciding which is canonical and removing the other.
In app/Services/PlanfactService.php at lines 2656–2676, inside parseQuoteDental(), the Rollover Amount is written to two field formats simultaneously:
- inNetwork{1,2}RolloverMaximum (legacy)
- inNetwork{1,2}RolloverVer2 + Type (new)
Comments in the source suggest the dual-write was a reversible migration safety net.
Please:
1. Show me the current dual-write.
2. Search the codebase (form templates, presentation rendering, exports, any Excel job) for which format is actually consumed.
3. If only Ver2 is consumed, recommend removing the legacy write here. If both are still used somewhere, document why with a comment.
4. Don't change anything yet — show the audit results first.
The frequency regex matches "N per [period]" but only handles 6 specific period strings. Phrasings like "every 12 months", "twice annually", or "2 per benefit year" may silently fail.
In app/Services/PlanfactService.php at lines 2997–3034, inside parseQuoteDental(), the Cleanings frequency regex matches "N per [period]" but only covers 6 specific period strings (calendar year, plan year, 6 Months, etc.). Common phrasings like "every 12 months", "twice annually", "2 per benefit year" may silently fail to match. Please: 1. Show me the current regex / switch. 2. Recommend a more flexible matcher that handles "every N months", "N per [period]", "twice/three times per [period]" — or a fallback path that captures the raw string and flags it for review. 3. Show me the proposed change.
Excess Responsibility is set to "None" for in-network and "Yes" for out-of-network unconditionally — not parsed from Plan Facts. This is correct in practice (balance billing is standard for dental), but the hardcoding is non-obvious. Worth a comment.
In app/Services/PlanfactService.php at lines 2555–2557, inside parseQuoteDental(), Excess Responsibility is set unconditionally: - in-network: "None" - out-of-network: "Yes" Not read from Plan Facts. Please: 1. Show me the assignment. 2. Add a 2-line code comment explaining why these are hardcoded (because dental balance-billing rules are standard and don't need per-plan extraction). 3. No behavior change.
Missing waiting period is skipped entirely, but an explicit "0" or "" sets Type to "None". Could mask a real data quality issue (e.g., the response is incomplete).
In app/Services/PlanfactService.php at lines 2875–2900, inside parseQuoteDental(), waiting period handling distinguishes: - field absent → skip entirely (no value set on Quote) - field present with empty / zero value → Type = "None" The first case might mask incomplete Plan Facts responses. Please: 1. Show me the current logic. 2. Recommend either: (a) treat absent same as zero (always set Type = "None" if no value), or (b) log a warning when the field is absent so we know if Plan Facts ever sends incomplete data. 3. Don't change yet — recommend.
Replace single-char "c" match with explicit case-insensitive "calendar year".
Audit which format is consumed; remove the legacy one if no longer used.
One-line comment.
Or add a fallback that preserves the raw string and flags it for review.
| File | Lines | Purpose |
|---|---|---|
| app/Services/PlanfactService.php | 149–159 | POST /dental endpoint config |
| app/Services/PlanfactService.php | 449 | Dispatch to parseQuoteDental |
| app/Services/PlanfactService.php | 2504–3059 | Full parser |
| app/Services/PlanfactService.php | 2645 | Deductible Period regex |
| app/Services/PlanfactService.php | 2656–2676 | Dual-stored Rollover |
| app/Services/PlanfactService.php | 2997–3034 | Cleanings frequency regex |
| app/Services/PlanfactService.php | 2555–2557 | Hardcoded Excess Responsibility |
| app/Services/PlanfactService.php | 55–60 | copayMap['dental'] |