From 07120ec062b428886f96b0416796128feceacaac Mon Sep 17 00:00:00 2001 From: Kisa Date: Tue, 2 Jun 2026 00:41:49 -0400 Subject: [PATCH] update CLAUDE.md and coverage calculator; add n8n content radar workflow, vercel SPA rewrite, and ui workspace config Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 112 +++++++++++++-------- n8n-workflows/content-radar.json | 1 + python-backend/core/coverage_calculator.py | 36 ++++++- signal-ui/pnpm-workspace.yaml | 2 +- signal-ui/vercel.json | 3 + 5 files changed, 112 insertions(+), 42 deletions(-) create mode 100644 n8n-workflows/content-radar.json create mode 100644 signal-ui/vercel.json diff --git a/CLAUDE.md b/CLAUDE.md index 37811d7..21cf39f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,11 +1,11 @@ -# STTIL Solutions — Signal | Level 1 Active Context +# STTIL Solutions — Signal | Active Build Context ## What This Tool Does -Signal is a B2B CGM documentation readiness tracker for DMEPOS suppliers. +Signal is a B2B DMEPOS documentation readiness tracker for CGM suppliers. It ingests order data from any DME order management CSV export (e.g., Brightree), evaluates each patient against documentation requirements (6-month qualifying -visit, PECOS enrollment, SWO, resupply eligibility), and produces a Signal Report -so supplier staff can prioritize outreach and capture revenue before it leaks. +visit, PECOS enrollment, SWO, resupply eligibility), and produces an actionable +worklist for supplier staff to prioritize outreach and capture revenue before it leaks. NOTE: Signal identifies gaps. It does not conduct outreach. Never write copy implying Signal contacts prescribers or patients directly. @@ -24,53 +24,85 @@ and stays predictable. quantity, payer — NOTHING else - All logs use hashed patient_id, never raw -## Level 1 Stack (Active) -- Hosting: Hostinger VPS (self-hosted, data never leaves VPS) -- Language: Python (FastAPI or Flask) -- Database: PostgreSQL (encrypted at rest, audit logs) -- Orchestration: n8n self-hosted (24-hr batch trigger) -- Notifications: Self-hosted Mailcow — encrypted email to DME staff only -- Frontend: Simple dashboard (Vercel or self-hosted) -- Auth: Session tokens in PostgreSQL (no Convex needed for MVP) +## Current Stack (Updated 2026-05-29) +- Backend: Railway (Python/FastAPI) — live at https://signal-api-production-91c2.up.railway.app +- Database: Supabase (PostgreSQL + RLS policies + Third Party Auth with Clerk) +- Auth: Clerk — app name: eternal-goblin-1, domain: eternal-goblin-1.clerk.accounts.dev +- Frontend: Vite/React (signal-ui) — local dev only at localhost:5173, NOT YET deployed publicly +- Orgs in Clerk: STTIL Solutions (default), Gaboro DME (org_3EPAEcAw06V2yGMSkxE3UjqIA3c) + +## Run Commands +- Frontend (local): `cd signal-ui && pnpm dev` → localhost:5173 +- Backend deploy (code change): `railway up --detach` from project root — forces fresh build +- Backend redeploy (no code change): `railway redeploy` — reuses existing image ## Active Files -- python-backend/core/coverage_calculator.py +- signal-ui/src/App.jsx — Clerk auth gate, OrganizationSwitcher +- signal-ui/src/components/WorklistTable.jsx — main worklist display +- signal-ui/src/components/CSVImport.jsx — CSV upload +- signal-ui/src/components/CSVExport.jsx — CSV export +- python-backend/core/coverage_calculator.py — gap detection logic (see Known Bugs) - python-backend/core/audit_logger.py +- python-backend/core/persistence.py +- python-backend/core/supabase_client.py +- python-backend/api/main.py +- python-backend/api/normalizer.py - python-backend/config/payer_rules.json -- research/dmepos-research-v3.md (market context) -- docs/hipaa-deployment-analysis-v1.md (compliance) -- docs/sttil-brand-system-v1.md (brand) -## Current BAA Obligations (Level 1 Only) -- Hostinger VPS: Request BAA (PHI host) -- Anthropic API: Request BAA if AI layer touches PHI -- Everything else: self-hosted = you are the operator +## Known Bugs — Fix Before Pilot +1. ~~**Payer matching**~~ — FIXED 2026-05-29. `_normalize_payer()` added to `coverage_calculator.py`. Handles: "Medicare Part B" → medicare, "Tricare for Life" / "TFL" → medicare, "Medicaid - GA" → medicaid, commercial insurer names → commercial. +2. **Visit date**: coverage_calculator.py uses shipment_date as proxy for qualifying visit, not an actual visit date. Fix: use stored confirmed visit date when available; fall back to shipment_date - 30 days labeled as estimated. -## Workload Impact Model (Build This) -Two-curve graph showing supplier staff time over months: -- Curve A: Appeals/denials workload (reactive) — starts HIGH, trends DOWN -- Curve B: Proactive outreach via Signal — starts MANAGEABLE, stays FLAT -- X-axis: Months 1–12 post-onboarding -- Crossover point = ROI moment to show on discovery calls +## Features Not Yet Built — Pilot Priority +- **Confirm Visit workflow**: staff button on each worklist row to record a confirmed visit date. Signal stores it against patient_id in Supabase. This is Signal's core persistent value — without it, visit dates never improve beyond estimates. +- **Frontend public URL**: DEPLOYED 2026-05-29 — https://signal-ui-xi.vercel.app +- **25 CSV variant test suite**: whitepaper requirement. Generate and test 25 mock CSVs with different column orders, headers, date formats, and payer name variants. +- **Mapping review interface**: confidence score display, user override dropdown per column (per whitepaper spec). -## What Is Tabled (Do Not Build Yet) +## Pilot Readiness — Authoritative Gauge +**Progress is measured against the whitepaper checklist only. Do not estimate % from feel.** -| Item | Phase | Reason deferred | -|------|-------|----------------| -| AWS infrastructure (any service) | Post-pilot | Jakub (coach, 2026-05-28): mock data is sufficient for demos and pilots. AWS takes 2 hours to set up — do it right before signing live PHI contracts. Do not start during MVP or pilot phase. | -| Dexcom OAuth API integration | Phase 2 | Requires vendor agreement + PHI scope expansion | -| Billing system API integration (Brightree, Bonafide, Fastrack, etc.) | Phase 2 | Build after pilot reveals which systems suppliers use; vendor agreements required; PHI scope expands significantly | -| CMS and contact management integration (Salesforce, etc.) | Phase 2 | Pilot feedback will reveal if suppliers have CMS and what they use; do not assume | -| Prescriber fax automation | Phase 2 | Workflow complexity; Level 1 manual outreach sufficient | -| Patient-facing SMS (Twilio, not SignalWire) | Phase 3 | PHI transmission to third-party; requires BAA + consent layer | -| Consortium strategy (Level 2/3) | 18–24 months | Depends on 15+ paying Level 1 suppliers first | -| Convex | Phase 2+ | Not needed until real-time sync requirements emerge | +Whitepaper: `/Users/sttil-solutions/Documents/Obsidian_Vault/STTIL-Vault/Projects/2026-05-18-pilot-readiness-whitepaper.md` + +### Checklist Status (as of 2026-05-29) — 40-45% pilot-ready + +| Checklist Item | Status | +|---|---| +| App hosted behind stable URL | PASS — https://signal-ui-xi.vercel.app (deployed 2026-05-29) | +| Demo login / controlled access working | PASS | +| Synthetic sample data loads end to end | NOT VERIFIED as Gaboro org | +| 25 CSV variants pass ingestion tests | NOT DONE | +| Import flow: mapping, validation, warnings | PARTIAL — CSVImport exists, full mapping review unconfirmed | +| Report generation calculates from backend | PASS | +| Each status has reason + recommended action | PARTIAL — payer bug fixed; visit date still uses shipment proxy | +| Export CSV works and opens cleanly | NOT VERIFIED end to end | +| Placeholder content removed | NOT VERIFIED | +| Browser smoke test passes | NOT RUN | +| Data handling rules documented | PASS | +| Real PHI blocked | PASS | +| Pilot success metrics written down | PASS | + +Update this table every build session. Re-count PASS items before quoting a % to anyone. + +## What Is Tabled — Do Not Build Yet + +| Item | Phase | Reason | +|---|---|---| +| AWS infrastructure | Post-pilot | Railway is sufficient until live PHI contracts signed | +| Dexcom OAuth API | Phase 2 | Requires vendor agreement + PHI scope expansion | +| Billing system API (Brightree, etc.) | Phase 2 | Build after pilot reveals which systems suppliers use | +| CMS / Salesforce integration | Phase 2 | Pilot feedback needed first | +| Prescriber fax automation | Phase 2 | Manual outreach sufficient for pilot | +| Patient-facing SMS | Phase 3 | PHI transmission requires BAA + consent layer | +| Doc upload + AI validation | Phase 2 | Explicitly deferred | +| Consortium strategy | 18-24 months | Need 15+ paying Level 1 suppliers first | ## Instructions for Claude Code Sessions -- Work only on Level 1 scope +- Work only on pilot-scope items from the checklist above - Never suggest adding PHI fields beyond patient_id - Flag any vendor integration that would require a new BAA - When building calculations, reference payer_rules.json for wear days - When building audit logs, always hash patient_id before storing -- If asked about Phase 2/3, acknowledge and defer — do not spec or build -- Use "resupply" not "refill". CGM supply allowances are exempt from Medicare refill rules +- Use "resupply" not "refill" — CGM supply allowances are exempt from Medicare refill rules +- Always calculate progress against the whitepaper checklist table above — not subjective feel +- Before quoting any % complete to Kisa, count the PASS items in the checklist diff --git a/n8n-workflows/content-radar.json b/n8n-workflows/content-radar.json new file mode 100644 index 0000000..553ad1e --- /dev/null +++ b/n8n-workflows/content-radar.json @@ -0,0 +1 @@ +{"updatedAt":"2026-06-01T14:16:54.303Z","createdAt":"2026-06-01T13:55:16.378Z","id":"11bgXkqYbqgXOGho","name":"STTIL Content Radar","description":null,"active":false,"isArchived":false,"nodes":[{"id":"radar-schedule","name":"Schedule 3x Daily","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[0,300],"parameters":{"rule":{"interval":[{"field":"cronExpression","expression":"0 11 * * *"},{"field":"cronExpression","expression":"0 17 * * *"},{"field":"cronExpression","expression":"0 23 * * *"}]}}},{"id":"radar-fetch","name":"Fetch RSS Feeds","type":"n8n-nodes-base.code","typeVersion":2,"position":[250,300],"parameters":{"mode":"runOnceForAllItems","language":"javaScript","jsCode":"\nconst feeds = [\n { url: 'https://homecaremag.com/news/feed', source: 'HomeCare Magazine' },\n { url: 'https://www.federalregister.gov/documents/search.rss?conditions%5Bterm%5D=DMEPOS', source: 'Federal Register' },\n { url: 'https://oig.hhs.gov/rss/fraud-alerts.xml', source: 'OIG Alerts' },\n { url: 'https://news.google.com/rss/search?q=%22continuous+glucose+monitor%22+Medicare+%22prior+authorization%22&hl=en-US&gl=US&ceid=US:en', source: 'Google News: CGM+PA' },\n { url: 'https://news.google.com/rss/search?q=DMEPOS+%22competitive+bidding%22+CGM&hl=en-US&gl=US&ceid=US:en', source: 'Google News: Competitive Bidding' },\n { url: 'https://news.google.com/rss/search?q=DMEPOS+supplier+Medicare&hl=en-US&gl=US&ceid=US:en', source: 'Google News: DMEPOS Medicare' }\n];\nconst items = [];\nfor (const feed of feeds) {\n try {\n const response = await fetch(feed.url, {\n headers: { 'User-Agent': 'Mozilla/5.0 (compatible; STTILBot/1.0)' },\n signal: AbortSignal.timeout(10000)\n });\n const text = await response.text();\n const itemMatches = text.match(/]*>([\\s\\S]*?)<\\/item>/gi) || [];\n for (const item of itemMatches.slice(0, 10)) {\n const getField = (tag, str) => {\n const m = str.match(new RegExp('<' + tag + '[^>]*>(?:)?<\\\\/' + tag + '>', 'i'));\n return m ? m[1].replace(/<[^>]+>/g,'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'\"').trim() : '';\n };\n const getLinkAtom = (str) => {\n const m = str.match(/]*>([^<]+)<\\/link>/i);\n return m ? m[1].trim() : '';\n };\n const title = getField('title', item);\n const link = getField('link', item) || getLinkAtom(item);\n const description = getField('description', item).substring(0, 400);\n const pubDate = getField('pubDate', item) || getField('published', item);\n if (title && link && link.startsWith('http')) {\n items.push({ json: { title, url: link, description, source: feed.source, pubDate, fetchedAt: new Date().toISOString() } });\n }\n }\n } catch(err) { /* skip failed feed */ }\n}\nreturn items;\n"}},{"id":"radar-keyword","name":"Keyword Filter","type":"n8n-nodes-base.code","typeVersion":2,"position":[500,300],"parameters":{"mode":"runOnceForAllItems","language":"javaScript","jsCode":"\nconst tier1 = ['prior authorization','prior auth','competitive bidding','dmepos moratorium','lcd update','cgm coverage','oig alert','cms-1828','cms 1828','pa exemption'];\nconst tier2 = ['dmepos','home medical equipment',' hme ','continuous glucose monitor',' cgm ','dexcom','freestyle libre','tandem diabetes','aahomecare','vgm group','medicare part b','claim denial','billing denial'];\nconst out = [];\nfor (const item of $input.all()) {\n const text = (item.json.title + ' ' + item.json.description).toLowerCase();\n const t1 = tier1.some(k => text.includes(k));\n const t2 = tier2.some(k => text.includes(k));\n if (t1 || t2) out.push({ json: { ...item.json, keywordTier: t1 ? 1 : 2 } });\n}\nreturn out;\n"}},{"id":"radar-dedup","name":"Deduplicate vs NocoDB","type":"n8n-nodes-base.code","typeVersion":2,"position":[750,300],"parameters":{"mode":"runOnceForAllItems","language":"javaScript","jsCode":"\nconst seenUrls = new Set();\ntry {\n const resp = await fetch(\n 'https://nocodb.sttilsolutions.com/api/v1/db/data/noco/pmf6r1bpy4gu47n/mw4420cgrtkelkc?limit=100&sort=-CreatedAt',\n { headers: { 'xc-token': 'b_KjcYI-g0XZ3farzuDvG1fvs4evv8WlnB8MOWC2' } }\n );\n const data = await resp.json();\n for (const row of (data.list || [])) {\n if (row['Published URL']) seenUrls.add(row['Published URL']);\n const notes = row['Notes'] || '';\n const m = notes.match(/SOURCE_URL: (https?:\\/\\/[^\\n]+)/);\n if (m) seenUrls.add(m[1].trim());\n }\n} catch(e) { /* continue without dedup */ }\nconst out = [];\nconst seenThisRun = new Set();\nfor (const item of $input.all()) {\n const url = item.json.url;\n if (!seenUrls.has(url) && !seenThisRun.has(url)) {\n seenThisRun.add(url);\n out.push(item);\n }\n}\nreturn out;\n"}},{"id":"radar-prep-relevance","name":"Prep Relevance Prompt","type":"n8n-nodes-base.code","typeVersion":2,"position":[1000,300],"parameters":{"mode":"runOnceForAllItems","language":"javaScript","jsCode":"\nreturn $input.all().map(item => ({\n json: {\n _src: item.json,\n prompt: `You are evaluating a news article for relevance to Signal, a DMEPOS supply continuity platform. Signal helps DMEPOS suppliers prevent CGM claim denials by tracking qualifying visit windows and documentation gaps. Audience: DMEPOS suppliers billing Medicare for CGM, and healthcare investors.\n\nArticle title: ${item.json.title}\nSource: ${item.json.source}\nDescription: ${item.json.description}\n\nRate relevance 1-5:\n1 = Unrelated\n2 = Tangential (general healthcare)\n3 = Moderate (Medicare/HME billing broadly)\n4 = High (DMEPOS, CGM, prior auth, Medicare Part B supplies)\n5 = Critical (CGM billing rules, DMEPOS supplier ops, PA exemptions, competitive bidding)\n\nRespond ONLY in this format:\nScore: [1-5]\nReason: [one sentence]`\n }\n}));\n"}},{"id":"radar-claude-score","name":"Claude: Rate Relevance","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1250,300],"parameters":{"method":"POST","url":"https://api.anthropic.com/v1/messages","authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth","sendHeaders":true,"headerParameters":{"parameters":[{"name":"anthropic-version","value":"2023-06-01"},{"name":"content-type","value":"application/json"}]},"sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({model:\"claude-haiku-4-5-20251001\",max_tokens:150,messages:[{role:\"user\",content:$json.prompt}]}) }}"},"credentials":{"httpHeaderAuth":{"id":"","name":"sttil-content-automation"}}},{"id":"radar-score-filter","name":"Filter Score >= 4","type":"n8n-nodes-base.code","typeVersion":2,"position":[1500,300],"parameters":{"mode":"runOnceForAllItems","language":"javaScript","jsCode":"\nconst anthropicItems = $input.all();\nconst prepItems = $('Prep Relevance Prompt').all();\nconst out = [];\nfor (let i = 0; i < anthropicItems.length; i++) {\n const raw = anthropicItems[i].json?.content?.[0]?.text || '';\n const scoreMatch = raw.match(/Score:\\s*([1-5])/i);\n const score = scoreMatch ? parseInt(scoreMatch[1]) : 0;\n const reasonMatch = raw.match(/Reason:\\s*(.+)/i);\n const reason = reasonMatch ? reasonMatch[1].trim() : 'Relevant to DMEPOS/CGM';\n if (score >= 4 && prepItems[i]) {\n out.push({ json: { ...prepItems[i].json._src, relevanceScore: score, relevanceReason: reason } });\n }\n}\nreturn out;\n"}},{"id":"radar-prep-copy","name":"Prep Copy Prompt","type":"n8n-nodes-base.code","typeVersion":2,"position":[1750,300],"parameters":{"mode":"runOnceForAllItems","language":"javaScript","jsCode":"\nreturn $input.all().map(item => ({\n json: {\n _src2: item.json,\n prompt: `You are drafting LinkedIn post options for Kisa Ndolo, founder of STTIL Solutions (Signal platform). Voice: verdict-first opener, short sentences after long ones, plain language, no em dashes, positive framing, \"you\" not \"companies\". Audience: DMEPOS suppliers and healthcare investors.\n\nNews item: ${item.json.title}\nSource: ${item.json.source}\nURL: ${item.json.url}\nDescription: ${item.json.description}\nWhy it matters: ${item.json.relevanceReason}\n\nWrite THREE distinct LinkedIn post options (150-220 words each):\n\nOPTION A — Problem frame: What this means for DMEPOS suppliers. Open with the consequence for them.\nOPTION B — Investor/market frame: What this signals about the market opportunity.\nOPTION C — Commentary/reaction: Kisa's direct take, more personal, first-person voice.\n\nRules: First line is the hook. End on consequence or a question. Include URL at end. Add exactly 2-3 hashtags from: #DMEPOS #CGM #MedicarePartB #HealthcareTechnology #HomemedicalEquipment\n\nFormat exactly:\nOPTION A:\n[post]\n\nOPTION B:\n[post]\n\nOPTION C:\n[post]`\n }\n}));\n"}},{"id":"radar-claude-copy","name":"Claude: Generate Copy","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2000,300],"parameters":{"method":"POST","url":"https://api.anthropic.com/v1/messages","authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth","sendHeaders":true,"headerParameters":{"parameters":[{"name":"anthropic-version","value":"2023-06-01"},{"name":"content-type","value":"application/json"}]},"sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({model:\"claude-sonnet-4-6\",max_tokens:1500,messages:[{role:\"user\",content:$json.prompt}]}) }}"},"credentials":{"httpHeaderAuth":{"id":"","name":"sttil-content-automation"}}},{"id":"radar-extract-copy","name":"Extract Copy Options","type":"n8n-nodes-base.code","typeVersion":2,"position":[2250,300],"parameters":{"mode":"runOnceForAllItems","language":"javaScript","jsCode":"\nconst anthropicItems = $input.all();\nconst prepItems = $('Prep Copy Prompt').all();\nconst out = [];\nfor (let i = 0; i < anthropicItems.length; i++) {\n const raw = anthropicItems[i].json?.content?.[0]?.text || '';\n const optA = (raw.match(/OPTION A[:\\s]*([\\s\\S]*?)(?=OPTION B|$)/i) || [])[1]?.trim() || '';\n const optB = (raw.match(/OPTION B[:\\s]*([\\s\\S]*?)(?=OPTION C|$)/i) || [])[1]?.trim() || '';\n const optC = (raw.match(/OPTION C[:\\s]*([\\s\\S]*?)$/i) || [])[1]?.trim() || '';\n if (prepItems[i]) {\n out.push({ json: { ...prepItems[i].json._src2, copyA: optA, copyB: optB, copyC: optC } });\n }\n}\nreturn out;\n"}},{"id":"radar-save-nocodb","name":"Save to NocoDB","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2500,300],"parameters":{"method":"POST","url":"https://nocodb.sttilsolutions.com/api/v1/db/data/noco/pmf6r1bpy4gu47n/mw4420cgrtkelkc","sendHeaders":true,"headerParameters":{"parameters":[{"name":"xc-token","value":"b_KjcYI-g0XZ3farzuDvG1fvs4evv8WlnB8MOWC2"},{"name":"Content-Type","value":"application/json"}]},"sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({\"Title\":$json.title,\"Content Type\":\"LinkedIn Post\",\"Status\":\"Idea\",\"Platform\":\"LinkedIn\",\"Notes\":\"SOURCE: \"+$json.source+\"\\nSOURCE_URL: \"+$json.url+\"\\nRELEVANCE (\"+$json.relevanceScore+\"/5): \"+$json.relevanceReason+\"\\n\\n--- OPTION A ---\\n\"+$json.copyA+\"\\n\\n--- OPTION B ---\\n\"+$json.copyB+\"\\n\\n--- OPTION C ---\\n\"+$json.copyC}) }}"}},{"id":"radar-telegram","name":"Telegram: Notify Kisa","type":"n8n-nodes-base.telegram","typeVersion":1.2,"position":[2750,300],"parameters":{"chatId":"8746737195","text":"=`\\U0001f4f0 *NEW CONTENT TRIGGER* (${$json.relevanceScore}/5)\\n\\n*${$json.title}*\\n\\n\\U0001f4cc Source: ${$json.source}\\n⭐ ${$json.relevanceReason}\\n\\n3 copy drafts saved to NocoDB ✔️\\nOpen Content Schedule, find by title to see A/B/C options.\\n\\n\\U0001f517 ${$json.url}`","additionalFields":{"parse_mode":"Markdown"}},"credentials":{"telegramApi":{"id":"XcSkhiJMwYJTPHUf","name":"STTIL Telegram Bot"}}}],"connections":{"Schedule 3x Daily":{"main":[[{"node":"Fetch RSS Feeds","type":"main","index":0}]]},"Fetch RSS Feeds":{"main":[[{"node":"Keyword Filter","type":"main","index":0}]]},"Keyword Filter":{"main":[[{"node":"Deduplicate vs NocoDB","type":"main","index":0}]]},"Deduplicate vs NocoDB":{"main":[[{"node":"Prep Relevance Prompt","type":"main","index":0}]]},"Prep Relevance Prompt":{"main":[[{"node":"Claude: Rate Relevance","type":"main","index":0}]]},"Claude: Rate Relevance":{"main":[[{"node":"Filter Score >= 4","type":"main","index":0}]]},"Filter Score >= 4":{"main":[[{"node":"Prep Copy Prompt","type":"main","index":0}]]},"Prep Copy Prompt":{"main":[[{"node":"Claude: Generate Copy","type":"main","index":0}]]},"Claude: Generate Copy":{"main":[[{"node":"Extract Copy Options","type":"main","index":0}]]},"Extract Copy Options":{"main":[[{"node":"Save to NocoDB","type":"main","index":0}]]},"Save to NocoDB":{"main":[[{"node":"Telegram: Notify Kisa","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":null,"meta":null,"pinData":null,"versionId":"7c5b7e66-4821-4dba-bd4a-ec14f59153f8","activeVersionId":null,"versionCounter":4,"triggerCount":0,"shared":[{"updatedAt":"2026-06-01T13:55:16.382Z","createdAt":"2026-06-01T13:55:16.382Z","role":"workflow:owner","workflowId":"11bgXkqYbqgXOGho","projectId":"AHrpwPVrGDiPmxMX","project":{"updatedAt":"2026-04-24T17:36:58.739Z","createdAt":"2026-01-03T16:28:54.334Z","id":"AHrpwPVrGDiPmxMX","name":"STTIL Solutions ","type":"personal","icon":null,"description":null,"creatorId":"c892947e-6f8b-4774-9d89-65a0649e6001","projectRelations":[{"updatedAt":"2026-01-03T16:28:54.334Z","createdAt":"2026-01-03T16:28:54.334Z","userId":"c892947e-6f8b-4774-9d89-65a0649e6001","projectId":"AHrpwPVrGDiPmxMX","user":{"updatedAt":"2026-06-01T06:29:47.000Z","createdAt":"2026-01-03T16:28:54.081Z","id":"c892947e-6f8b-4774-9d89-65a0649e6001","email":"sttilsolutionsllc@proton.me","firstName":"STTIL","lastName":"Solutions","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2026-01-04T01:46:00.027Z","personalization_survey_n8n_version":"2.1.5","companySize":"<20","companyType":"systems-integrator","role":"business-owner","reportedSource":"youtube"},"settings":{"userActivated":true,"firstSuccessfulWorkflowId":"kt0IHRdMrUh5wp6G","userActivatedAt":1777058999059},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2026-06-01","isPending":false}}]}}],"tags":[],"activeVersion":null} \ No newline at end of file diff --git a/python-backend/core/coverage_calculator.py b/python-backend/core/coverage_calculator.py index 20ffedc..f766639 100644 --- a/python-backend/core/coverage_calculator.py +++ b/python-backend/core/coverage_calculator.py @@ -97,9 +97,43 @@ def _get_wear_days(rules: dict, device_type: str, component: str) -> int: return wear_days +_COMMERCIAL_KEYWORDS = { + "aetna", "united", "unitedhealthcare", "cigna", "humana", + "bcbs", "blue cross", "blue shield", "anthem", "molina", + "centene", "wellcare", "oscar", "ambetter", "carefirst", + "highmark", "geisinger", "kaiser", "commercial", +} + + +def _normalize_payer(payer: str) -> str: + """ + Map raw DME CSV payer strings to canonical payer_rules.json keys. + + "Medicare Part B" → "medicare" + "Medicaid - GA" → "medicaid" + "Aetna PPO" → "commercial" + + Preserves original value in CoverageResult.payer for display. + """ + s = payer.strip().lower() + if "medicare" in s: + return "medicare" + if "tricare for life" in s or s == "tfl": + return "medicare" + if "medicaid" in s: + return "medicaid" + if "tricare" in s: + return "commercial" + for keyword in _COMMERCIAL_KEYWORDS: + if keyword in s: + return "commercial" + return s + + def _get_payer_config(rules: dict, payer: str) -> dict: payer_rules = rules.get("payer_rules", {}) - return payer_rules.get(payer.lower(), payer_rules.get("default", {})) + normalized = _normalize_payer(payer) + return payer_rules.get(normalized, payer_rules.get("default", {})) def _compute_priority(flag: CoverageFlag, days_until_end: int) -> int: diff --git a/signal-ui/pnpm-workspace.yaml b/signal-ui/pnpm-workspace.yaml index d2a1a1f..3649153 100644 --- a/signal-ui/pnpm-workspace.yaml +++ b/signal-ui/pnpm-workspace.yaml @@ -1,2 +1,2 @@ allowBuilds: - '@clerk/shared': set this to true or false + '@clerk/shared': true diff --git a/signal-ui/vercel.json b/signal-ui/vercel.json new file mode 100644 index 0000000..0f32683 --- /dev/null +++ b/signal-ui/vercel.json @@ -0,0 +1,3 @@ +{ + "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] +}