+
AI-powered PDF → structured-data parser. Two services in concert — Planfacts API (Python wrapper) and OpenPlan (Python schema definitions). Field-by-field audit across all 10 benefit types. v2 reads from both sides — Plansight (Laravel) and the API (Python) — so most "verify with Plan Facts" items from v1 are now confirmed or refuted with ground-truth evidence.
The parser dispatch pattern, the staging step through QuoteAi->aiResponseParsed, the per-benefit parser separation, and the multi-network detection in medical/dental are all well-designed. The four likely-bug findings are concrete one-line or few-line fixes — none require structural change.
| Question from v1 | v2 ruling |
|---|---|
| Hospital Indemnity — does the API have a separate Substance Abuse field? | Yes — Plansight bug confirmed The API returns Other Conditions.[Class].Substance Abuse as a dedicated key. Plansight reads from Mental/Nervous for both fields. |
| Vision — does the API consistently use "In-Network" / "Out-of-Network"? | Yes — assumption confirmed The API hardcodes those exact two network names. |
| Vision — are out-of-network "Discount Beyond Allowance" fields returned? | No — Plansight is correct The API genuinely doesn't return these for OON; Plansight not mapping them is intentional, not a gap. |
| Critical Illness — is "Angioplasty" separate or nested under Coronary Arteriosclerosis? | Separate top-level key The cross-mapping to criticalIllnessCoronaryArteriosclerosis is a Quote-side naming choice, not an API-level confusion. |
| Critical Illness — Childhood conditions structure? | Presence flag with "Initial" amount Plansight's binary read is approximately correct; tightening for explicit "No" / "Not Included" values is still useful. |
| Voluntary Life — does the API return separate Smoker / Non-Smoker rates? | No — uni-smoker only The API uses Unismoker (singular). Plansight's approach matches. |
| Group Life — Contribution Type enum values? | Confirmed Noncontributory, Contributory, Voluntary, Gross-up, Tax choice. Plansight's enum reversal at line 1052 is still a bug. |
| Disability — does the API natively return per-class data? | Yes for everything The API returns per-class data on every benefit field. Plansight's intentional collapse to plan-level on most fields (per the domain model) is a deliberate downstream decision. |
This audit is a snapshot of Plansight APP 9d1afd54d5df, Planfacts API 0b2d9a11ef3f, and OpenPlan 270e51fe72de. Two enhancements are imminent and worth flagging now even though they aren't yet in the GitHub repo:
.xlsx and .docx as inputs alongside PDF — the same parser pipeline, just more file types upstream. This widens what brokers can drop in (carrier proposal spreadsheets, redacted Word benefit summaries) without re-keying.Both features will appear in the next Bitbucket pull. When they land, this audit should refresh the cover blurb, the "How to read" section, and add per-benefit notes about which fields the hover-trace surfaces.
Click any benefit to open its detail page — same audit shape on each: per-class handling, what's mapped, fields the API returns we don't read, potentially-mapped-wrong items with Cursor prompts inline.
POST /medical · parseQuoteMedical()
Multi-network parse (1–3 networks). Items: enum casing, network sort, drug tier cap, $0 deductibles.
POST /dental · parseQuoteDental()
Multi-network with annual max, rollover, waiting periods. Items: brittle regex, dual-stored rollover.
POST /vision · parseQuoteVision()
Fixed 2-network parse — confirmed correct. Items: dual-key Lenses, Yes/No matchers, Disposable Contacts.
POST /std · parseQuoteStDisability()
One plan, three per-class variables (benefit %, max, tax). Domain model implemented correctly.
POST /ltd · parseQuoteLtDisability()
Like ST plus 3 LT extras to confirm. Line 3676 cross-write to ST field.
POST /accident · parseQuoteAccident()
Single-class. Surgery-Benefit field naming question; Emergency Room missing Type field.
POST /criticalIllness · parseQuoteCriticalIllness()
Single-class with ~20 conditions. Angioplasty mapping; binary Childhood Conditions; Skin Cancer Type fragility.
POST /hospitalIndemnity · parseQuoteHospitalIndemnity()
Confirmed bug: Substance Abuse reads from Mental/Nervous source. API has dedicated key.
POST /life_add · parseQuoteGroupLife()
Writes to disabilityWaiverWaitingPeriod (Disability field) inside Life parser. Contribution Type semantic reversal.
POST /life_add · parseQuoteVoluntaryLife()
Confirmed bug: line 805 missing dot in path string — Spouse Benefit Amount silently fails.
Four shapes that show up in multiple parsers. Worth knowing so the same fix doesn't have to be discovered three times.
Field-name cross-writes appear in three parsers
Group Life writes to disabilityWaiverWaitingPeriod (a Disability field name) at line 1140. Hospital Indemnity reads from the Mental/Nervous source path for both Mental/Nervous AND Substance Abuse (line 1847). LT Disability at line 3676 writes the LT Definition of Disability into stDisabilityDefinitionOfDisability (the ST field). All three have the same root cause: copy-paste between similar parsers without renaming the destination. Worth a one-time grep sweep across PlanfactService.php for any other places where one parser assigns to another's field prefix.
Path-string concatenation is fragile
Several parsers concatenate Plan Facts response paths as strings — e.g. 'X.' . $class . '.Y'. Voluntary Life line 805 has a missing-dot bug that breaks the Spouse Benefit Amount lookup silently. Worth a sweep for similar mistakes elsewhere — anywhere a variable is followed by another quoted string with no leading dot is suspect.
Enum mapping is mostly case-sensitive
Some fields use strtolower() before matching, others don't. The Medical Plan Type switch at line 3991 is case-sensitive while neighboring enums in the same parser are not. A Plan Facts response with unusual casing ("hmo" vs "HMO") could silently fail to match without warning.
The Substance Abuse pattern matters beyond Hospital Indemnity
The bug pattern of "two consecutive blocks both reading the same source path, writing to two different Quote fields" is worth checking elsewhere. The fix for Hospital Indemnity is a one-line source-path change. A grep for similar shapes would surface any other instances.
Eight sections per benefit page. The most useful sections for a developer are "Potentially mapped incorrectly" (each item has a Cursor prompt) and "Fields Plan Facts returns that Plansight doesn't read."
| Section | What you'll find |
|---|---|
| Bottom line | One sentence: what to walk away with for this benefit. |
| Per-class / per-tier handling | How the parser handles classes / tiers / networks, and how that maps to Plansight's domain model. |
| What's mapped | Every Plan Facts field that flows into a Quote field, with file:line. Confirmed against the API source where possible. |
| Plan-level (by design) | Fields the API returns per-class where Plansight intentionally collapses to plan-level. |
| Fields Plan Facts returns that Plansight doesn't read | New in v2 — derived from cross-referencing the API schema. Candidates for ingestion or explicit "we don't need this" decisions. |
| Potentially mapped incorrectly | Cross-writes, enum mismatches, regex gaps, dropped fields the form expects. Each item includes a copy-pasteable Cursor prompt. |
| Manual-entry-only fields | Quote form fields the broker has to fill in because the API doesn't return them. |
| Recommendations | 2–4 small concrete items per benefit. No framework refactors, no unified parsers. |
Quick file:line lookup. Line numbers may drift between Bitbucket pulls — verify against current code with grep before applying any prompt.
| File | Lines | Purpose |
|---|---|---|
| app/Services/PlanfactService.php | 132–304 | API endpoint configs |
| app/Services/PlanfactService.php | 446–465 | parseQuote() dispatch switch |
| app/Services/PlanfactService.php | 469–970 | parseQuoteVoluntaryLife() + line 805 path bug |
| app/Services/PlanfactService.php | 972–1362 | parseQuoteGroupLife() + Contribution Type / disabilityWaiver concerns |
| app/Services/PlanfactService.php | 1365–1701 | parseQuoteAccident() |
| app/Services/PlanfactService.php | 1703–1921 | parseQuoteHospitalIndemnity() + Substance Abuse cross-write at 1847 |
| app/Services/PlanfactService.php | 1923–2310 | parseQuoteCriticalIllness() + Angioplasty mapping at 2108 |
| app/Services/PlanfactService.php | 2313–2443 | parseQuoteVision() |
| app/Services/PlanfactService.php | 2504–3059 | parseQuoteDental() |
| app/Services/PlanfactService.php | 3061–3447 | parseQuoteStDisability() |
| app/Services/PlanfactService.php | 3449–3951 | parseQuoteLtDisability() + line 3676 cross-write |
| app/Services/PlanfactService.php | 3984–4507 | parseQuoteMedical() |
| app/Services/PlanfactService.php | 4509–4687 | parseMoney() and parseCopay() helpers |
| app/Http/Controllers/PlanfactsController.php | 165 | Stage parsed result on QuoteAi->aiResponseParsed |
| app/Http/Controllers/Rest/App/QuoteController.php | 501 | $model->fill(...) to Quote |
| File | Line | Purpose |
|---|---|---|
| main.py | 6909 | POST /medical |
| main.py | 7351 | POST /life_add |
| main.py | 7592 | POST /std |
| main.py | 8055 | POST /ltd |
| main.py | 8973 | POST /vision |
| main.py | 10398 | POST /ci |
| main.py | 11153 | POST /hi |
| File | Lines | Purpose |
|---|---|---|
| sources_simple.py | 702–708 | Medical Networks (5 keys: IN1/IN2/IN3/OON/OOA) |
| sources_simple.py | 900 | Medical Drug Tiers (no explicit cap) |
| sources_simple.py | 1079–1108 | Critical Illness covered conditions |
| sources_simple.py | 1166–1173 | Hospital Indemnity Other Conditions (incl. dedicated Substance Abuse) |
| sources_simple.py | 1327–1363 | STD per-class structure |
| sources_simple.py | 1446–1500 | Vision schema (hardcoded networks; OON Discount fields absent) |
| sources_simple.py | 1646–1785 | Voluntary Life rates structure (Unismoker) |
| sources_simple.py | 2751–2775 | Group Life Contribution Type enum |
~/plansight - Bit Bucket Pull 4.26/. v2 reads from ~/Library/CloudStorage/Dropbox-Steveo/Plansight Dev/Bit Bucket Repos/, refreshed weekly.python3 scripts/pull-repos.py)BUILD.md, recomputes everything, regenerates these pagesvercel --prod to shipPlanfactService.php is the right way to enumerate them precisely.