Plan Facts Parsing Analysis · v1
Multi-network parse (1–3 networks) detecting plan type, HDHP status, deductibles, OOP maximums, specialist referral rules, ~25 service-level copays, drug tiers (up to 6), and four-tier composite rates.
Largest parser in the file. Six small items worth a verify.
The multi-network detection, copay parsing, and drug-tier handling are sophisticated. The items below are mostly defensive: enum case sensitivity, a 6-tier hard cap on drug tiers, a network-sorting heuristic that's fragile at 4+ networks, and a non-obvious "[service]Other" form contract.
Multi-network architecture, not multi-class. The parser detects 1, 2, or 3 networks and stores fields under inNetwork / outNetwork prefixes:
No employee-class concept. Rates are coverage-tier (Employee Only, +Spouse, +Child, +Children, Family) — line 4487.
| Plan Facts field | Quote field | Notes |
|---|---|---|
| Plan Name | name | Direct (line 3988) |
| Plan Type (HMO/PPO/EPO/POS/Indemnity/RBP) | medicalPlanType | ⚠ Case-sensitive enum (line 3991) |
| HDHP (yes/no) | hsa | Normalized Yes/No (line 4002) |
| Networks | inNetworkNetwork | + multi-network sort logic (line 4014) |
| Overall Deductible.[Network].Individual / Family | inNetworkDeductibleIndividual, …Family, outNetworkDeductibleIndividual, …Family | Numeric, $ stripped (lines 4072–4157) |
| Overall Deductible.[Network].Deductible Type | inNetworkDeductibleType, outNetworkDeductibleType | Embedded / Aggregate (case-insensitive) |
| Overall Deductible.[Network].Deductible Frequency | inNetworkDeductibleOopAccumulation, outNetworkDeductibleOopAccumulation | "calendar year" / "plan year" |
| Out-of-Pocket Maximum.[Network].Individual / Family | inNetworkOOPIndividual, …Family, outNetworkOOPIndividual, …Family | Numeric (lines 4196–4253) |
| Specialist Referral Rule.[Network].Specialist Referral Rule | inNetworkSpecialistReferral, outNetworkSpecialistReferral | Yes / No (line 4256) |
| [Service from copayMap].[Network].Payment (~25 services) | inNetwork[Service] + outNetwork[Service] + Type / Other suffixes | Via parseCopay() — handles $, %, "Up to $", "Not Covered", ranges (lines 4293–4387) |
| [Service].[Network].Deductible Applies | inNetwork[Service]Ded, outNetwork[Service]Ded | "No" → ND; "Yes"/"After Deductible" → AD (line 4379) |
| Drugs.Drug Tiers (list) | RxTier1Label – RxTier6Label | ⚠ Capped at 6 tiers (line 4484) |
| Drugs.[TierName].[Network].Payment | inNetworkRxTier{1..6}, outNetworkRxTier{1..6} | Via parseCopay() (line 4413) |
| Drugs.[TierName].[Network].Deductible Applies | inNetworkRxTier{1..6}Ded, outNetworkRxTier{1..6}Ded | "nd" / "ad" (line 4458) |
| Rates.[Coverage Tier] | rateEmployeeOnly, rateEmployeeSpouse, rateEmployeeChild, rateEmployeeChildren, rateEmployeeFamily | Numeric, $ + comma stripped (lines 4487–4504) |
Plan Type is matched case-sensitively. If Plan Facts returns "hmo" or "Hmo" instead of "HMO", the parse fails silently and medicalPlanType is left empty. Several other enums in this method are normalized with strtolower() — this one isn't.
In app/Services/PlanfactService.php at lines 3991–3999, inside parseQuoteMedical(), the Plan Type switch is case-sensitive. Other enum mappings in the same method use strtolower() to normalize before matching. Please: 1. Show me the Plan Type switch. 2. Add strtolower() to the input value before the switch (matching the pattern used elsewhere in this parser). 3. Make sure the case statements use lowercase. 4. Show me the proposed change.
When a Plan Facts response has more than 2 networks, the parser uses a deductible-based sort heuristic that only ever extracts 3 specific networks (index 0, 1, last). Middle networks (4+) are silently dropped. Two networks with identical deductibles can also be mis-labeled as "in-network" vs "out-of-network".
In app/Services/PlanfactService.php at lines 4026–4067, inside parseQuoteMedical(), the multi-network handling uses a deductible-based sort heuristic when there are more than 2 networks. The current logic: - Builds an associative array from networks[0], networks[1], and networks[count-1] only. - Sorts by deductible. Issues: - 4+ networks: middle networks are silently ignored. - Networks with equal deductibles: ordering is undefined. Please: 1. Show me the current logic. 2. Recommend an improvement: either explicitly limit to 3 networks at parse time with a log warning when more are encountered, OR find a more reliable signal than deductible (e.g., network name pattern matching). 3. Don't change anything yet — show the proposed approach.
When a 3+ network plan has two in-network copays for the same service, the parser combines them with a "/" separator into a field named [service]Other and sets [service]Type = "Other". The base [service] field is left unset. Form code has to know to read [service]Other when [service]Type === "Other" — and the contract isn't documented in code comments.
In app/Services/PlanfactService.php at lines 4313–4348, inside parseQuoteMedical(), there's a non-obvious contract for multi-network copay combining: - When type is "Other", the actual value is in [service]Other (not [service]). - Form / display code must check Type first to know which field to read. Please: 1. Show me the current logic. 2. Add an inline comment block (4–6 lines) explaining the contract and pointing at the frontend file that consumes it. 3. Don't change behavior — just document.
Drug tier parsing hard-stops at 6 tiers (if ($tierNum > 6) break;). If a carrier returns a 7th tier (specialty injectables, biosimilars, etc.), it's silently dropped. Worth confirming whether 6 is enough in practice.
In app/Services/PlanfactService.php at line 4484, inside parseQuoteMedical(), drug tier parsing hard-stops at 6 tiers. Tiers beyond the 6th are silently dropped. Please: 1. Show me the line. 2. Check the Quote model and form template — is RxTier defined for tiers 1–6 only, or higher? 3. If the limit is real (form only supports 6), add a log warning when more tiers are encountered, so we know if this becomes an issue. 4. If higher tiers are supported on the form but not in the parser, raise the parser's limit.
Several extraction blocks use if ($ded) to check for presence — which is falsy when the value is "0". A $0 deductible (valid for some plans) might be skipped, leaving the Quote field empty rather than recording $0.
In app/Services/PlanfactService.php inside parseQuoteMedical(), several deductible extractions use truthy checks like:
if ($ded) { ... }
PHP treats "0" as falsy, so a $0 deductible value would be skipped. $0 deductibles are valid (e.g., HSA-paired or first-dollar plans).
Please:
1. Find every instance of this pattern in parseQuoteMedical() and the helper functions it calls (parseMoney, parseCopay).
2. Replace truthy checks with explicit "is not null and is not empty string" checks where the field semantically allows "0" as a valid value.
3. Show me the affected lines and the proposed change.
Medical's network detection is robust (it reads the carrier's network names), but several downstream blocks build keys like 'Overall Deductible.' . $networkName . '...'. If the carrier uses unusual punctuation or whitespace in network names, those concatenations could fail. Mostly defensive — worth a one-time audit of test responses.
In app/Services/PlanfactService.php inside parseQuoteMedical() (lines 4072 onward), many lookups concatenate the network name into a Plan Facts path key: $value = $data['Overall Deductible'][$networkName]['Overall Individual Deductible'] ?? null; If a carrier's network name contains unusual whitespace or characters that don't match what's in the response, the lookup silently fails. Please: 1. Find all such concatenations in this parser. 2. Wrap them in a helper that, on lookup failure, logs the network name being searched and the available keys at that level. This makes future failures debuggable instead of silent. 3. Show me the proposed helper and an example of how to apply it.
Apply strtolower() consistent with other enums in this parser.
Inline comment block. No behavior change.
Minor — these become silent data drops today.
Several spots in this parser drop $0 because of if ($ded). Replace with explicit null-check.
| File | Lines | Purpose |
|---|---|---|
| app/Services/PlanfactService.php | 132–146 | POST /medical endpoint config |
| app/Services/PlanfactService.php | 447 | Dispatch to parseQuoteMedical |
| app/Services/PlanfactService.php | 3984–4507 | Full parser |
| app/Services/PlanfactService.php | 3991–3999 | Plan Type enum (case sensitivity) |
| app/Services/PlanfactService.php | 4014–4067 | Network sorting heuristic |
| app/Services/PlanfactService.php | 4293–4387 | Service-level copay parsing |
| app/Services/PlanfactService.php | 4391–4485 | Drug tier parsing (6-tier cap at line 4484) |
| app/Services/PlanfactService.php | 4555–4687 | parseCopay() helper (regex patterns for $, %, ranges) |
| app/Services/PlanfactService.php | 4509–4553 | parseMoney() helper |
| app/Services/PlanfactService.php | 25–79 | copayMap definition |