Compare commits
3 commits
242e54e01a
...
5ba31fac03
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ba31fac03 | ||
|
|
bd92126384 | ||
|
|
d24340c47a |
19 changed files with 1069 additions and 64 deletions
|
|
@ -4,7 +4,7 @@ v:1
|
||||||
|
|
||||||
ACTIVE: 2026-07-07 [CC]. SIGNAL BUILD UN-PAUSED — readiness-model completion in flight per docs/build-plans/. P1 (wiring) SHIPPED + LIVE-VERIFIED (commit a3af499, deployed to Railway, false-green kill observed live). Executing P2 (citation IDs) -> P3 (rollup) -> P4 (device overrides) -> P5 (plan-type enforcement) next, one item at a time with independent audit each. P6 (frontend nesting) BLOCKED on Kisa approving the rendered mockup (built, awaiting render+review).
|
ACTIVE: 2026-07-07 [CC]. SIGNAL BUILD UN-PAUSED — readiness-model completion in flight per docs/build-plans/. P1 (wiring) SHIPPED + LIVE-VERIFIED (commit a3af499, deployed to Railway, false-green kill observed live). Executing P2 (citation IDs) -> P3 (rollup) -> P4 (device overrides) -> P5 (plan-type enforcement) next, one item at a time with independent audit each. P6 (frontend nesting) BLOCKED on Kisa approving the rendered mockup (built, awaiting render+review).
|
||||||
|
|
||||||
NEXT: (CC) P2-P5 per docs/build-plans/02-readiness-model-completion.md; (Kisa) approve/reject P6 mockup decisions A-D; (Kisa) confirm OQ1 plan_type-ingestion decision recorded 2026-07-07 (shipped per umbrella plan; two-line revert if reversed); (Kisa) attorney email bundle + Gaboro re-approach per reactivation plan 00 (unchanged). Queued behind Signal: UMSIS docs-only activation, "63% written off" Draft fix.
|
NEXT: (CC) P2-P5 per docs/build-plans/02-readiness-model-completion.md; (Kisa) approve/reject P6 mockup decisions A-D; (Kisa) confirm OQ1 plan_type-ingestion decision recorded 2026-07-07 (shipped per umbrella plan; two-line revert if reversed); (Kisa) attorney bundle READY TO SEND (signal/pitch/legal/attorney-bundle-2026-07-07, 5 Word docs + sendable memo): confirm recipient (Clyde Mathes vs Bittinger/Nixon), entity name + formation state, arbitration-vs-courts posture, Partner Concepts carve-out keep/drop, then email; Gaboro re-approach per reactivation plan 00 (unchanged). Queued behind Signal: UMSIS docs-only activation, "63% written off" Draft fix.
|
||||||
|
|
||||||
PRIOR ACTIVE: 2026-06-28. Signal paused at clean checkpoint — Insight Engine in active BUILD phase for July 4 deadline. See insight-engine/context/current-state.md and insight-engine/handoff-clock.md for full state. Key progress today: x-sight guided experience designed (Compass) and approved by Kisa, lane-lock handoff clock created, frontend finish readiness assessed (15 ranked items). Claude is building the Compass landing + B1 deploy blocker + finish items. Signal READINESS MODEL still queued at the clean checkpoint from 2026-06-27.
|
PRIOR ACTIVE: 2026-06-28. Signal paused at clean checkpoint — Insight Engine in active BUILD phase for July 4 deadline. See insight-engine/context/current-state.md and insight-engine/handoff-clock.md for full state. Key progress today: x-sight guided experience designed (Compass) and approved by Kisa, lane-lock handoff clock created, frontend finish readiness assessed (15 ranked items). Claude is building the Compass landing + B1 deploy blocker + finish items. Signal READINESS MODEL still queued at the clean checkpoint from 2026-06-27.
|
||||||
|
|
||||||
|
|
@ -25,6 +25,7 @@ Insight Engine (July 4 deadline = tomorrow) is the active project. Key files:
|
||||||
|
|
||||||
## Decisions (last
|
## Decisions (last
|
||||||
|
|
||||||
|
- 2026-07-07 [CC]: Attorney-review legal bundle PREPARED (lead-sttil drafted, ops-steward independently reviewed, 6 findings fixed incl. 4 HIGH counterparty-name leaks). LOI template v3 (product-agnostic body, Exhibit A pre-filled for Signal, labABLE negotiation learnings folded in), NDA template v3 (products list corrected to Signal/Vitality Med Tracker/UMSIS + catch-all), BAA template v1 (generalized from Gaboro draft, forward-looking posture block), cover memo bundling both templates + BAA + FDA CDS memo + labABLE AKS question per reactivation plan 00. Entity name corrected everywhere to KJF Professional Services LLC dba STTIL Solutions (per executed labABLE LOI; kg_add recorded); formation state left as placeholder (old docs disagree: Delaware/Florida/Pennsylvania) = attorney Q1. Templates: STTIL-Vault/Projects/Legal/ (LOI+NDA v3) + signal/pitch/legal/ (BAA, memo). Send-ready docx: signal/pitch/legal/attorney-bundle-2026-07-07/.
|
||||||
- 2026-07-07 [CC]: Readiness model WIRED LIVE (plan 01, commit a3af499, Railway deploy verified). Phase 2a seam confirmed: readiness AUGMENTS legacy output (byte-identical legacy proven by live diff). OQ1 executed per umbrella plan 00 ("ship plan_type ingestion with the wiring") — minimal client-mapped column, never guessed; flagged for Kisa's explicit confirmation, two-line revert if reversed. Adversarial review caught a false-green in the draft row-resolution; fixed via dedup-group-keyed ReadinessIndex, regression-locked. Grounded test count: 132 (the old "73 tests" figure corrected per plan 01 §12.5).
|
- 2026-07-07 [CC]: Readiness model WIRED LIVE (plan 01, commit a3af499, Railway deploy verified). Phase 2a seam confirmed: readiness AUGMENTS legacy output (byte-identical legacy proven by live diff). OQ1 executed per umbrella plan 00 ("ship plan_type ingestion with the wiring") — minimal client-mapped column, never guessed; flagged for Kisa's explicit confirmation, two-line revert if reversed. Adversarial review caught a false-green in the draft row-resolution; fixed via dedup-group-keyed ReadinessIndex, regression-locked. Grounded test count: 132 (the old "73 tests" figure corrected per plan 01 §12.5).
|
||||||
- 2026-07-06 [CC]: Model routing SETTLED (Kisa agreed) — strongest model in the orchestrator seat (plain claude, full MCP, skip-permissions), sonnet/haiku subagents for mechanical work, Fable-as-subagent only for cleanly separable hard problems. Docs-verified: delegation multiplies tokens on one shared pool; per-agent model override + mid-session /model GROUNDED. Config application PARKED. Discovered com.sttil.cos-model-switch: Fable-to-Opus swap fires 2026-07-07 09:00. Watcher audit: 9 agents healthy, clean exits; research watcher never built.
|
- 2026-07-06 [CC]: Model routing SETTLED (Kisa agreed) — strongest model in the orchestrator seat (plain claude, full MCP, skip-permissions), sonnet/haiku subagents for mechanical work, Fable-as-subagent only for cleanly separable hard problems. Docs-verified: delegation multiplies tokens on one shared pool; per-agent model override + mid-session /model GROUNDED. Config application PARKED. Discovered com.sttil.cos-model-switch: Fable-to-Opus swap fires 2026-07-07 09:00. Watcher audit: 9 agents healthy, clean exits; research watcher never built.
|
||||||
- 2026-07-06 [CC]: Crew Compounding Memory Phase 1 shipped + independently verified — retrieve-and-inject wired into the dispatch skill (validity-stamped "WHAT THE CREW ALREADY KNOWS" block, current==true filter, recall guard) + ~/.claude/crew-log.md; built by the memory-crew, verified PASS (2 agents flagged every planted contradiction, independent grader confirmed). Also ruled: UMSIS activates sttil/umsis docs-only (state in the repo, not the vault). Spec + build record: STTIL-Vault/Projects/STTIL Solutions/Crew Compounding Memory - Design Spec - 2026-07-05.md.
|
- 2026-07-06 [CC]: Crew Compounding Memory Phase 1 shipped + independently verified — retrieve-and-inject wired into the dispatch skill (validity-stamped "WHAT THE CREW ALREADY KNOWS" block, current==true filter, recall guard) + ~/.claude/crew-log.md; built by the memory-crew, verified PASS (2 agents flagged every planted contradiction, independent grader confirmed). Also ruled: UMSIS activates sttil/umsis docs-only (state in the repo, not the vault). Spec + build record: STTIL-Vault/Projects/STTIL Solutions/Crew Compounding Memory - Design Spec - 2026-07-05.md.
|
||||||
|
|
@ -58,4 +59,4 @@ Insight Engine (July 4 deadline = tomorrow) is the active project. Key files:
|
||||||
|
|
||||||
State protocol: read = `git pull --rebase` then read this file (warn if Updated > 24h old); write = edit ACTIVE/NEXT, append one author-stamped decision, then commit + push. Full protocol in the `state-sync` skill. Detailed ship-status (what shipped / didn't / why) = docs/ship-status-ledger.md. TriLane = searchable archive, not live state.
|
State protocol: read = `git pull --rebase` then read this file (warn if Updated > 24h old); write = edit ACTIVE/NEXT, append one author-stamped decision, then commit + push. Full protocol in the `state-sync` skill. Detailed ship-status (what shipped / didn't / why) = docs/ship-status-ledger.md. TriLane = searchable archive, not live state.
|
||||||
|
|
||||||
## Updated: 2026-07-07 (CC: Signal un-paused; P1 readiness wiring shipped+live-verified a3af499; P2-P5 executing; P6 mockup awaiting Kisa; OQ1 recorded for confirmation.)
|
## Updated: 2026-07-07 (CC: Signal un-paused; P1 readiness wiring shipped+live-verified a3af499; P2-P5 executing; P6 mockup awaiting Kisa; OQ1 recorded for confirmation; attorney legal bundle prepared and ready to send.)
|
||||||
|
|
|
||||||
224
pitch/legal/STTIL-BAA-template-v1.md
Normal file
224
pitch/legal/STTIL-BAA-template-v1.md
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
# BUSINESS ASSOCIATE AGREEMENT (Template v1)
|
||||||
|
|
||||||
|
> **DRAFT, for attorney review before use. Not legal advice.**
|
||||||
|
> Prepared 2026-07-07. Generalized from the a prior supplier-specific BAA into a reusable STTIL Solutions template. This is a FORWARD-LOOKING instrument. See "Status and posture" immediately below before reading the operative terms.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STATUS AND POSTURE (read first, attorney)
|
||||||
|
|
||||||
|
This BAA template is prepared as a forward-looking instrument, not a description of the current live environment. As of 2026-07-07:
|
||||||
|
|
||||||
|
- Real PHI is BLOCKED. The Product operates on mock and de-identified data today; no real protected health information is in active exposure (Signal current-state.md, 2026-07-07).
|
||||||
|
- The current pilot stack (Supabase, Railway, Vercel, Clerk) does NOT have executed subprocessor BAAs in place. The subprocessor table in Section 2.5 describes the pilot stack, not a BAA-covered chain.
|
||||||
|
- AWS is the compliant migration path. An AWS BAA was signed and became active 2026-05-27; RDS and S3 are provisioned but parked, not in active build until post-funding. If and when a Covered Entity relationship requires real PHI, the Product migrates to the AWS (or equivalent BAA-capable) environment before any PHI is processed, and the subprocessor table is updated accordingly.
|
||||||
|
|
||||||
|
Counsel should review this template as the instrument STTIL would execute at the point real PHI enters the system, and advise on the threshold question in Attorney Review Note 1 (whether a BAA is required at all given the crosswalk-stays-with-supplier architecture).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Effective Date:** __________ , 20__
|
||||||
|
|
||||||
|
**Between:**
|
||||||
|
- **KJF Professional Services LLC d/b/a STTIL Solutions** ("Business Associate"), a [STATE, confirm formation state with counsel] limited liability company, Kisa Fenn, Managing Member
|
||||||
|
- **[COVERED ENTITY FULL LEGAL NAME]** ("Covered Entity"), [SIGNER NAME], [SIGNER TITLE]
|
||||||
|
|
||||||
|
Each a "Party" and together the "Parties."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recitals
|
||||||
|
|
||||||
|
Covered Entity is [a durable medical equipment supplier / a healthcare provider / other] subject to the Health Insurance Portability and Accountability Act of 1996 and its implementing regulations ("HIPAA"). Business Associate provides a documentation readiness platform (the "Service"), described in the Pilot Letter of Intent between the Parties (the "LOI") and its Exhibit A. In connection with the Service, Business Associate may receive, create, maintain, or transmit data on behalf of Covered Entity that may constitute Protected Health Information ("PHI") as defined under HIPAA. The Parties enter into this Agreement to satisfy the Business Associate Agreement requirements of 45 C.F.R. Part 164, Subpart C.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Definitions
|
||||||
|
|
||||||
|
Terms used in this Agreement have the meanings assigned under HIPAA and its implementing regulations, including the HITECH Act amendments, as of the Effective Date.
|
||||||
|
|
||||||
|
**"Protected Health Information" or "PHI"** means individually identifiable health information created, received, maintained, or transmitted by Business Associate on behalf of Covered Entity, in any form or medium.
|
||||||
|
|
||||||
|
**"Minimum Necessary"** means the least amount of PHI required to accomplish the intended purpose of a given use, disclosure, or request.
|
||||||
|
|
||||||
|
**"Security Incident"** means the attempted or successful unauthorized access, use, disclosure, modification, or destruction of information, or interference with system operations in an information system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Obligations of Business Associate
|
||||||
|
|
||||||
|
### 2.1 Permitted Uses and Disclosures
|
||||||
|
|
||||||
|
Business Associate may use or disclose PHI only:
|
||||||
|
|
||||||
|
(a) To perform the Service described in the LOI and its Exhibit A, specifically: receiving order-management data uploaded by Covered Entity limited to the minimum-necessary fields specified in the LOI, calculating documentation readiness status per record, and returning a prioritized worklist to Covered Entity staff;
|
||||||
|
|
||||||
|
(b) As required by law; or
|
||||||
|
|
||||||
|
(c) For the proper management and administration of Business Associate's own operations, provided that any such disclosure is required by law or Business Associate obtains reasonable assurances that the information will be held in confidence and used or further disclosed only as required by law or for the purpose for which it was disclosed.
|
||||||
|
|
||||||
|
Business Associate will not use or disclose PHI in any manner that would violate HIPAA if done by Covered Entity directly.
|
||||||
|
|
||||||
|
### 2.2 Minimum Necessary
|
||||||
|
|
||||||
|
Business Associate will make reasonable efforts to use, disclose, and request only the minimum necessary PHI to accomplish the intended purpose.
|
||||||
|
|
||||||
|
### 2.3 Safeguards
|
||||||
|
|
||||||
|
Business Associate will implement and maintain appropriate administrative, physical, and technical safeguards to protect the confidentiality, integrity, and availability of PHI it creates, receives, maintains, or transmits on behalf of Covered Entity, in accordance with 45 C.F.R. Part 164, Subpart C (Security Rule).
|
||||||
|
|
||||||
|
Business Associate's safeguards include:
|
||||||
|
|
||||||
|
- HTTPS enforced on all data transmission surfaces
|
||||||
|
- SHA-256 hashing of patient_id before storage in any log or database record
|
||||||
|
- Row Level Security restricting data access by organization
|
||||||
|
- JWT authentication required on all API endpoints
|
||||||
|
- No storage of patient names, dates of birth, Social Security numbers, addresses, or contact information at any time
|
||||||
|
- The patient identity crosswalk (supplier's internal patient_id to real patient identity) is maintained solely by Covered Entity staff and is never transmitted to or stored by Business Associate
|
||||||
|
|
||||||
|
*Attorney note: this safeguards list reflects the current pilot architecture. When the Service migrates to a BAA-capable host for real PHI (see Status and posture), this list is updated to reflect the production environment, including encryption at rest.*
|
||||||
|
|
||||||
|
### 2.4 Reporting
|
||||||
|
|
||||||
|
Business Associate will report to Covered Entity, without unreasonable delay and in no event later than 30 calendar days of discovery:
|
||||||
|
|
||||||
|
(a) Any use or disclosure of PHI not permitted by this Agreement of which Business Associate becomes aware;
|
||||||
|
|
||||||
|
(b) Any Security Incident of which Business Associate becomes aware, including breaches of Unsecured PHI as required by 45 C.F.R. Part 164, Subpart D;
|
||||||
|
|
||||||
|
(c) Any attempted but unsuccessful Security Incident of which Business Associate becomes aware, to the extent practicable.
|
||||||
|
|
||||||
|
Reports will be made to Covered Entity's designated contact in writing.
|
||||||
|
|
||||||
|
### 2.5 Subcontractors
|
||||||
|
|
||||||
|
Business Associate will ensure that any subcontractor or agent to which it provides PHI on behalf of Covered Entity agrees in writing to the same restrictions, conditions, and requirements that apply to Business Associate under this Agreement, before any PHI is provided to that subcontractor.
|
||||||
|
|
||||||
|
Business Associate's current subprocessors are:
|
||||||
|
|
||||||
|
| Subprocessor | Purpose | Data Touched |
|
||||||
|
|---|---|---|
|
||||||
|
| Supabase | PostgreSQL database hosting | Hashed patient_id, device_type, shipment_date, payer, doc status fields |
|
||||||
|
| Railway | Backend API hosting | Processes upload requests in transit; does not persist PHI |
|
||||||
|
| Vercel | Frontend hosting | No PHI; serves static application only |
|
||||||
|
| Clerk | Staff authentication | No PHI; stores staff identity only |
|
||||||
|
|
||||||
|
*Attorney note: none of the subprocessors listed above currently has an executed subprocessor BAA. Real PHI is blocked today (see Status and posture). Before any real PHI is processed, Business Associate will either (a) obtain an executed BAA from each subprocessor that will touch PHI, or (b) migrate the PHI-touching functions to a BAA-capable environment (an AWS BAA is signed and active as of 2026-05-27; AWS RDS and S3 are provisioned and parked). This table is updated to reflect the production chain at that point.*
|
||||||
|
|
||||||
|
Business Associate will notify Covered Entity of any material change to subprocessors that will touch PHI, and will obtain Covered Entity's written approval before adding any new subprocessor that will receive or process PHI.
|
||||||
|
|
||||||
|
### 2.6 Access to PHI
|
||||||
|
|
||||||
|
To the extent Business Associate maintains PHI in a Designated Record Set, Business Associate will make such PHI available to Covered Entity as necessary to fulfill Covered Entity's obligations under 45 C.F.R. Section 164.524 within 15 business days of a written request.
|
||||||
|
|
||||||
|
### 2.7 Amendment
|
||||||
|
|
||||||
|
To the extent Business Associate maintains PHI in a Designated Record Set, Business Associate will make such PHI available for amendment and will incorporate any amendments requested by Covered Entity as required by 45 C.F.R. Section 164.526.
|
||||||
|
|
||||||
|
### 2.8 Accounting of Disclosures
|
||||||
|
|
||||||
|
Business Associate will maintain a record of disclosures of PHI and will make such information available to Covered Entity as necessary to respond to a request for an accounting of disclosures under 45 C.F.R. Section 164.528.
|
||||||
|
|
||||||
|
### 2.9 Government Access
|
||||||
|
|
||||||
|
Business Associate will make its internal practices, books, and records relating to the use and disclosure of PHI available to the Secretary of the U.S. Department of Health and Human Services for purposes of determining compliance with HIPAA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Obligations of Covered Entity
|
||||||
|
|
||||||
|
Covered Entity agrees to:
|
||||||
|
|
||||||
|
(a) Notify Business Associate of any limitation in Covered Entity's Notice of Privacy Practices that would affect Business Associate's permitted uses or disclosures;
|
||||||
|
|
||||||
|
(b) Notify Business Associate of any changes in, or revocations of, authorization by individuals that would affect Business Associate's permitted uses or disclosures;
|
||||||
|
|
||||||
|
(c) Not request Business Associate to use or disclose PHI in any manner that would not be permissible under HIPAA if done by Covered Entity directly;
|
||||||
|
|
||||||
|
(d) Ensure that any data uploaded to the Service is limited to the minimum-necessary fields specified in the LOI: an internal patient identifier (patient_id), device type, shipment date, quantity, and payer name, plus any optional documentation-status fields the Parties agree to. Covered Entity will not upload patient names, Social Security numbers, dates of birth, addresses, or direct contact information to the Service at any time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Term and Termination
|
||||||
|
|
||||||
|
### 4.1 Term
|
||||||
|
|
||||||
|
This Agreement is effective as of the Effective Date and continues until the later of: (a) the termination of the LOI or any subsequent formal agreement between the Parties; or (b) the date on which Business Associate has completed its data return and destruction obligations under Section 4.3.
|
||||||
|
|
||||||
|
### 4.2 Termination for Cause
|
||||||
|
|
||||||
|
Either Party may terminate this Agreement immediately upon written notice to the other Party if the other Party has materially breached a provision of this Agreement and has not cured the breach within 10 business days of written notice identifying the breach.
|
||||||
|
|
||||||
|
### 4.3 Return or Destruction of PHI
|
||||||
|
|
||||||
|
Upon termination of this Agreement for any reason:
|
||||||
|
|
||||||
|
(a) Business Associate will return to Covered Entity or destroy all PHI received from or created on behalf of Covered Entity, including PHI in the possession of subcontractors, within 10 business days of the effective termination date;
|
||||||
|
|
||||||
|
(b) Business Associate will certify in writing to Covered Entity that all such PHI has been returned or destroyed;
|
||||||
|
|
||||||
|
(c) If return or destruction is not feasible, Business Associate will extend the protections of this Agreement to such PHI and limit further use or disclosure to the purposes that make return or destruction infeasible, for as long as Business Associate maintains the PHI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Miscellaneous
|
||||||
|
|
||||||
|
### 5.1 Regulatory References
|
||||||
|
|
||||||
|
Any reference in this Agreement to a section of HIPAA includes the most recent amendment or regulatory guidance applicable to that section.
|
||||||
|
|
||||||
|
### 5.2 Interpretation
|
||||||
|
|
||||||
|
This Agreement is intended to comply with HIPAA as amended. Where this Agreement conflicts with HIPAA, HIPAA controls. Where this Agreement is silent, HIPAA controls.
|
||||||
|
|
||||||
|
### 5.3 No Third-Party Beneficiaries
|
||||||
|
|
||||||
|
Nothing in this Agreement confers any right, remedy, or claim upon any third party, including any patient whose PHI is involved.
|
||||||
|
|
||||||
|
### 5.4 Governing Law
|
||||||
|
|
||||||
|
This Agreement is governed by the laws of the State of [STATE, confirm formation state with counsel], without regard to conflict of law principles.
|
||||||
|
|
||||||
|
### 5.5 Entire Agreement
|
||||||
|
|
||||||
|
This Agreement, together with the LOI and the mutual NDA between the Parties, constitutes the entire agreement with respect to PHI and supersedes all prior understandings on the subject.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Signatures
|
||||||
|
|
||||||
|
**KJF Professional Services LLC d/b/a STTIL Solutions (Business Associate)**
|
||||||
|
By: ___________________________
|
||||||
|
Name: Kisa Fenn
|
||||||
|
Title: Managing Member
|
||||||
|
Date: _________________________
|
||||||
|
Email: [KISA EMAIL]
|
||||||
|
|
||||||
|
**[COVERED ENTITY FULL LEGAL NAME] (Covered Entity)**
|
||||||
|
By: ___________________________
|
||||||
|
Name: [SIGNER NAME]
|
||||||
|
Title: [SIGNER TITLE]
|
||||||
|
Date: _________________________
|
||||||
|
Email: ___________________________
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Attorney Review Notes
|
||||||
|
|
||||||
|
Each load-bearing factual claim is labeled GROUNDED (cited source) or GRAFTED (assumption for counsel to confirm). Items to confirm before this Agreement is executed with any Covered Entity:
|
||||||
|
|
||||||
|
1. **LOAD-BEARING: Is a BAA required at all? (GRAFTED as applied.)** Confirm whether Business Associate's receipt of Covered Entity's internal patient_id (a supplier account number, one of HIPAA's 18 identifiers) makes STTIL a Business Associate requiring this Agreement, or whether the Service's de-identification architecture (no names, no DOBs, no SSNs; the crosswalk stays with Covered Entity) satisfies the Safe Harbor or Expert Determination de-identification standards such that no BAA is triggered. GROUNDED that patient_id is an account number and the sole crosswalk key, and that names/DOBs/SSNs are refused at the normalizer (Signal CLAUDE.md, PHI Architecture; data-handling.md). Whether that architecture avoids Business Associate status is GRAFTED and is the single most important question in this bundle.
|
||||||
|
|
||||||
|
2. **Subprocessor agreements (GROUNDED posture, GRAFTED requirement).** GROUNDED that the current pilot stack (Supabase, Railway, Vercel, Clerk) has no executed subprocessor BAAs and that an AWS BAA is signed and active as of 2026-05-27 (Signal current-state.md; KG Signal). Confirm which of the subprocessors, in the production PHI configuration, legally requires its own BAA, and confirm the AWS migration path satisfies the subcontractor obligation in 45 C.F.R. 164.308(b) and 164.314.
|
||||||
|
|
||||||
|
3. **Accounting of disclosures scope (GRAFTED as applied).** Confirm the scope of Section 2.8 as applied to a software service that processes de-identified or minimum-necessary data only.
|
||||||
|
|
||||||
|
4. **State-specific requirements (GRAFTED).** Confirm which state's health-data statutes (beyond HIPAA) apply once formation state and Covered Entity location are known. A prior supplier-specific version governed under Pennsylvania; this template uses a placeholder. Confirm no additional state-law obligations attach.
|
||||||
|
|
||||||
|
5. **Governing law and entity alignment (GROUNDED that documents disagree).** The v1 LOI and NDA said Delaware; the a prior supplier BAA said Pennsylvania; an executed letter of intent states Florida. Entity name is now "KJF Professional Services LLC d/b/a STTIL Solutions." Confirm the correct legal name and formation state and align Section 5.4 with the LOI and NDA.
|
||||||
|
|
||||||
|
6. **Forward-looking status (GROUNDED).** Real PHI is blocked/mock today (Signal current-state.md, 2026-07-07). Confirm this template is the correct instrument to execute at the point real PHI enters the system, and advise whether any interim data-handling addendum is needed for a pilot that stays inside the de-identified minimum-necessary field set before that point.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Document prepared for attorney review. Not legal advice.*
|
||||||
|
*Related: STTIL-pilot-LOI-template-v3, STTIL-NDA-template-v3*
|
||||||
109
pitch/legal/STTIL-attorney-review-cover-memo-2026-07-07.md
Normal file
109
pitch/legal/STTIL-attorney-review-cover-memo-2026-07-07.md
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
> **DRAFT, for attorney review before use. Not legal advice.**
|
||||||
|
> Cover memo prepared 2026-07-07. Bundles five items for a single review pass.
|
||||||
|
|
||||||
|
**To:** [ATTORNEY NAME, CONFIRM RECIPIENT before sending. Session records dated 2026-06-05 name Clyde Mathes as attorney of record; the Signal compliance checklist rows name Bittinger Law / Nixon Law Group. These conflict. Reconcile before this memo goes out.]
|
||||||
|
**From:** Kisa Fenn, Founder and CEO, KJF Professional Services LLC d/b/a STTIL Solutions
|
||||||
|
**Date:** [DATE]
|
||||||
|
**Re:** Single-pass review of three template agreements plus two focused legal questions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Hello [ATTORNEY NAME],
|
||||||
|
|
||||||
|
I would like to engage you for one bundled review pass covering five items. I have grouped them deliberately so you can scope the whole thing at once rather than in pieces. An associate handling the bulk of this is completely fine with me. What I need first is a turnaround estimate and a fee estimate so I can plan around it.
|
||||||
|
|
||||||
|
Here is what is in the bundle:
|
||||||
|
|
||||||
|
1. **Pilot Letter of Intent (LOI) template (v3)**, a product-agnostic pilot LOI. Product specifics live in an Exhibit A, pre-filled for our active product, Signal.
|
||||||
|
2. **Mutual Non-Disclosure Agreement (NDA) template (v3)**, portfolio-wide.
|
||||||
|
3. **Business Associate Agreement (BAA) template (v1)**, a reusable BAA, generalized from an earlier supplier-specific draft. Please read it as a forward-looking instrument; I explain why below.
|
||||||
|
4. **U.S. Food and Drug Administration (FDA) device-status memo for Signal**, a draft I am providing for your review, not asking you to write. It argues that Signal is not an FDA-regulated medical device. It is candid about its weakest points. I need you to confirm, correct, or reject its position.
|
||||||
|
5. **Anti-Kickback Statute question** on a specific partner integration, described below. This one is time-sensitive because it gates whether we can deepen an existing partner relationship.
|
||||||
|
|
||||||
|
I have written my own focused questions under each item. Please treat these as a starting list, not a limit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Overarching question (applies to all three templates)
|
||||||
|
|
||||||
|
**Q1. Confirm our legal name, our state of formation, and align governing law across every document.** Our entity is KJF Professional Services LLC d/b/a STTIL Solutions. Our documents currently disagree on the state: two older templates say Delaware, an executed letter of intent says Florida, and an older business associate agreement says Pennsylvania. I have replaced the state with a placeholder everywhere so nothing is asserted incorrectly. Please tell me the correct legal name and formation state, and set governing law and dispute-resolution venue consistently across the LOI, NDA, and BAA.
|
||||||
|
|
||||||
|
**Q2. Standardize my signing title.** The LOI and NDA sign as "Founder and CEO" while the BAA signs as "Managing Member." Tell me which title is correct for an LLC signer and I will use it across all three.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Pilot LOI template (v3)
|
||||||
|
|
||||||
|
1. The BAA cross-reference in Section 5.3 turns on whether we are a Business Associate at all. That is the BAA question below; please keep the two consistent.
|
||||||
|
2. Section 8.3 (we retain all rights to any modification or enhancement made during a pilot, regardless of who requested it) and Section 8.4 (a narrow carve-out for a partner's pre-documented pre-existing concepts). Confirm both are enforceable and that 8.4 does not erode 8.3.
|
||||||
|
3. Section 12.3, a new mutual exclusion of consequential damages for a no-cost pilot. Confirm it is adequate and enforceable.
|
||||||
|
4. Section 14, a dispute-resolution path (30-day good-faith discussion, then arbitration, with the American Arbitration Association (AAA) as fallback, and a court carve-out for injunctive relief). We added this after an out-of-state counterparty objected to exclusive Delaware court jurisdiction. Tell me whether to keep arbitration here and whether to match it in the NDA, which currently uses courts.
|
||||||
|
5. Sections 6.1 and 13.3, confirming that a successful pilot does not obligate us to offer a commercial agreement. Confirm this closes any promissory-estoppel exposure.
|
||||||
|
|
||||||
|
### 2. Mutual NDA template (v3)
|
||||||
|
|
||||||
|
1. The residuals clause (Articles 1.2 and 2.3), which carves our algorithms, payer and coverage logic, and scoring formulas out of what a counterparty may reuse from memory. Confirm the carve-out is enforceable.
|
||||||
|
2. The Health Insurance Portability and Accountability Act (HIPAA) carve-out (Article 6). I softened the flat claim that the data we handle is never protected health information (PHI). Confirm the new framing is defensible.
|
||||||
|
3. The tightened third-party exclusion (Article 3.1(d)). Confirm it closes the loophole cleanly and applies equally to both sides.
|
||||||
|
4. The named-products list (Article 1.1(c)): Signal, Vitality Med Tracker, UMSIS, plus a catch-all for any current or future offering. Confirm the catch-all is sufficient to cover future products we have not named.
|
||||||
|
|
||||||
|
### 3. BAA template (v1)
|
||||||
|
|
||||||
|
1. **The single most important question in this whole bundle:** does our receipt of a supplier's internal patient_id, which is an account number and one of HIPAA's 18 identifiers, make us a Business Associate that needs this agreement, or does our architecture (we never receive names, dates of birth, or Social Security numbers, and the identity crosswalk stays with the supplier) avoid Business Associate status? Everything downstream depends on your answer.
|
||||||
|
2. Please read this as forward-looking. Real patient data is blocked today; we run on mock and de-identified data. Our current pilot vendors do not have subprocessor BAAs. Our compliant path is AWS, where we already have a signed BAA (active since 2026-05-27) with database and storage provisioned and parked until funding. I want you reviewing the instrument we would sign at the moment real data enters, and advising whether we need any interim addendum before then.
|
||||||
|
3. Whether each pilot vendor legally needs its own subprocessor BAA in the production configuration.
|
||||||
|
|
||||||
|
### 4. FDA device-status memo for Signal (for your review, not to be drafted)
|
||||||
|
|
||||||
|
The memo is at `signal/docs/compliance/fda-cds-exemption-memo-DRAFT.md`. I am not asking you to write it; I am asking you to rule on it. Its own author flagged these as the points to press on:
|
||||||
|
|
||||||
|
1. Confirm or correct the primary position: Signal is not a device under section 201(h) because its intended use is administrative and billing support, with the section 520(o)(1)(A) administrative-support exclusion as reinforcement.
|
||||||
|
2. Rule on whether a DMEPOS supplier can rely on 520(o)(1)(A) as a "health care facility," or whether the position must rest on the 201(h) intended-use analysis alone.
|
||||||
|
3. Review our live worklist status labels, reason strings, and "recommended action" strings verbatim, and tell us if any wording reads as clinical rather than documentation-focused.
|
||||||
|
4. Approve a short list of permitted marketing claim framings so our intended use stays administrative.
|
||||||
|
5. Confirm the memo's citations against FDA's Clinical Decision Support (CDS) guidance current at the time of your review (it was verified against the January 29, 2026 version).
|
||||||
|
6. Advise whether, once settled, this should be reissued as a counsel letter we can hand to supplier compliance contacts and investors.
|
||||||
|
|
||||||
|
### 5. Anti-Kickback Statute question (time-sensitive, privileged, confidential)
|
||||||
|
|
||||||
|
**Confidentiality note:** the partner named here is bound to us by a confidentiality clause that bars public mention of the relationship. I am sharing the name with you only because you are our counsel and I need the facts to get sound advice. Please keep it inside privilege.
|
||||||
|
|
||||||
|
We have an executed 180-day discovery letter of intent with Pathogens Artificial Intelligence Inc., which does business as LabABLE, a diagnostic-laboratory software platform run by Dr. Mehul Patel's team. During discovery we discussed a reciprocal integration concept, informally called "kit in kit": LabABLE could be made available as a component inside our Signal platform, and Signal could be made available as a component inside the LabABLE platform.
|
||||||
|
|
||||||
|
Both of our customer bases serve providers who bill federal healthcare programs (our suppliers bill Medicare for DMEPOS; laboratories bill Medicare Part B). My question:
|
||||||
|
|
||||||
|
1. Does this reciprocal embedding or cross-availability arrangement, where each party's product carries value into the other's platform and could influence orders or referrals of federally reimbursable items or services, implicate the federal Anti-Kickback Statute (42 U.S.C. 1320a-7b(b)), the Eliminating Kickbacks in Recovery Act, or the Stark Law?
|
||||||
|
2. If so, what structure or safe harbor would we need before executing any definitive agreement with this partner?
|
||||||
|
|
||||||
|
This must be resolved before we execute any further agreement with them, so please tell me how heavily it weighs on your timeline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### What I need back
|
||||||
|
|
||||||
|
- A turnaround estimate and a fee estimate for the full bundle.
|
||||||
|
- Your answers, or corrections, to the questions above.
|
||||||
|
- A flag on anything I have framed wrong or missed.
|
||||||
|
|
||||||
|
Thank you. I am happy to get on a call to walk through any of it.
|
||||||
|
|
||||||
|
Kisa Fenn
|
||||||
|
KJF Professional Services LLC d/b/a STTIL Solutions
|
||||||
|
[KISA EMAIL] / [PHONE]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ATTORNEY REVIEW NOTES (for internal use, do not send)
|
||||||
|
|
||||||
|
Load-bearing facts in this memo, labeled GROUNDED (cited source) or GRAFTED (assumption to confirm):
|
||||||
|
|
||||||
|
- Recipient is unresolved. GROUNDED that 2026-06-05 records name Clyde Mathes and the Signal compliance checklist names Bittinger/Nixon; the two conflict (Signal CLAUDE.md; FDA memo routing note). Confirm before sending. GRAFTED which is correct.
|
||||||
|
- labABLE LOI executed 2026-06-12, 180-day discovery term. GROUNDED (KG Signal; executed PDF `signal/pitch/legal/labABLE-LOI-v3-EXECUTED-2026-06-12.pdf`). Verify signer names and clause numbers against the executed PDF before any citation; the "kit-in-kit" description here is from the 2026-06-06 markup, not the final executed text.
|
||||||
|
- labABLE legal entity is Pathogens Artificial Intelligence Inc. d/b/a LabABLE. GROUNDED (labABLE LOI markup 2026-06-06).
|
||||||
|
- AWS BAA signed and active 2026-05-27, RDS and S3 provisioned and parked. GROUNDED (KG Signal; Signal current-state.md).
|
||||||
|
- Real PHI blocked/mock today. GROUNDED (Signal current-state.md 2026-07-07).
|
||||||
|
- FDA memo location and its own self-identified weak points. GROUNDED (`fda-cds-exemption-memo-DRAFT.md`).
|
||||||
|
- The AKS/EKRA/Stark characterization is a lay framing of the question, not a legal conclusion. GRAFTED, for counsel to analyze.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Prepared for attorney review. Not legal advice.*
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -180,6 +180,39 @@ class ReadinessItemOut(BaseModel):
|
||||||
quality: str # GOOD | PENDING | BAD | NONE
|
quality: str # GOOD | PENDING | BAD | NONE
|
||||||
value: Optional[str] = None
|
value: Optional[str] = None
|
||||||
satisfied: bool
|
satisfied: bool
|
||||||
|
# Citation: the payer_rules.json rule that decided required_state.
|
||||||
|
# None when NOT_EVALUATED (no rule fired, nothing to cite).
|
||||||
|
rule_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class LineOut(BaseModel):
|
||||||
|
"""One coverage line (patient x device x plan type) in the nested payload."""
|
||||||
|
|
||||||
|
device_type: str
|
||||||
|
device_display: str
|
||||||
|
plan_type: Optional[str] = None # None = plan type needed (never guessed)
|
||||||
|
gradeable: bool
|
||||||
|
line_status: Optional[str] = None # LineStatus verbatim; None = ungraded
|
||||||
|
timing_flag: Optional[str] = None # CoverageFlag of the current shipment
|
||||||
|
doc_items: Optional[list[ReadinessItemOut]] = None
|
||||||
|
# Display fields per the locked dedup design (section 6)
|
||||||
|
order_numbers: list[str] = []
|
||||||
|
hcpcs_codes: list[str] = []
|
||||||
|
total_quantity: int = 0
|
||||||
|
shipment_date: Optional[str] = None # current shipment DOS
|
||||||
|
payer: Optional[str] = None
|
||||||
|
prior_shipment_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class PatientOut(BaseModel):
|
||||||
|
"""One patient in the nested payload; tabs count these, not rows (P6)."""
|
||||||
|
|
||||||
|
patient_id: str
|
||||||
|
rollup_status: Optional[str] = None # None = no graded gradeable lines
|
||||||
|
plan_type_needed: bool = False
|
||||||
|
cgm_line_count: int = 0
|
||||||
|
total_line_count: int = 0
|
||||||
|
lines: list[LineOut] = []
|
||||||
|
|
||||||
|
|
||||||
class RecordOut(BaseModel):
|
class RecordOut(BaseModel):
|
||||||
|
|
@ -231,6 +264,10 @@ class UploadResponse(BaseModel):
|
||||||
mapping_summary: dict
|
mapping_summary: dict
|
||||||
batch_id: str | None = None
|
batch_id: str | None = None
|
||||||
readiness_stats: dict | None = None
|
readiness_stats: dict | None = None
|
||||||
|
# Nested patient payload (P3, additive). The flat `records` list stays
|
||||||
|
# untouched until P6 switches the UI (locked migration strategy).
|
||||||
|
patients: list[PatientOut] | None = None
|
||||||
|
patient_stats: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
def _build_reason(
|
def _build_reason(
|
||||||
|
|
@ -330,6 +367,24 @@ class DocStatusRequest(BaseModel):
|
||||||
expiry_date: Optional[str] = None
|
expiry_date: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _readiness_items_out(readiness) -> Optional[list[ReadinessItemOut]]:
|
||||||
|
"""Serialize a ReadinessVerdict's doc items (shared by rows and lines)."""
|
||||||
|
if not readiness:
|
||||||
|
return None
|
||||||
|
return [
|
||||||
|
ReadinessItemOut(
|
||||||
|
doc_type=i.doc_type,
|
||||||
|
required_state=i.required_state.value,
|
||||||
|
input_state=i.input_state.value,
|
||||||
|
quality=i.quality.value,
|
||||||
|
value=i.value,
|
||||||
|
satisfied=i.satisfied,
|
||||||
|
rule_id=i.rule_id,
|
||||||
|
)
|
||||||
|
for i in readiness.doc_items
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _to_record_out(
|
def _to_record_out(
|
||||||
r,
|
r,
|
||||||
record=None,
|
record=None,
|
||||||
|
|
@ -419,19 +474,7 @@ def _to_record_out(
|
||||||
else None,
|
else None,
|
||||||
plan_type=getattr(record, "plan_type", None) if record else None,
|
plan_type=getattr(record, "plan_type", None) if record else None,
|
||||||
readiness_status=readiness.line_status.value if readiness else None,
|
readiness_status=readiness.line_status.value if readiness else None,
|
||||||
readiness_items=[
|
readiness_items=_readiness_items_out(readiness),
|
||||||
ReadinessItemOut(
|
|
||||||
doc_type=i.doc_type,
|
|
||||||
required_state=i.required_state.value,
|
|
||||||
input_state=i.input_state.value,
|
|
||||||
quality=i.quality.value,
|
|
||||||
value=i.value,
|
|
||||||
satisfied=i.satisfied,
|
|
||||||
)
|
|
||||||
for i in readiness.doc_items
|
|
||||||
]
|
|
||||||
if readiness
|
|
||||||
else None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -457,6 +500,104 @@ def _compute_stats(records: list[RecordOut]) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_patients_payload(index, reachable_line_keys, results):
|
||||||
|
"""Assemble the nested patients payload (P3) from the lines reachable in
|
||||||
|
THIS response, so patients/lines always reconcile with the flat records.
|
||||||
|
Rollup rules are locked in core/rollup.py (worst gradeable line; PTN caps
|
||||||
|
at Action Needed; non-gradeable lines excluded from the light)."""
|
||||||
|
from core.dedup import UNKNOWN_PLAN_TYPE
|
||||||
|
from core.rollup import rollup_patients
|
||||||
|
|
||||||
|
reachable = {
|
||||||
|
k: index.lines[k] for k in reachable_line_keys if k in index.lines
|
||||||
|
}
|
||||||
|
if not reachable:
|
||||||
|
return [], {"patients_total": 0}
|
||||||
|
|
||||||
|
from core.dedup import _as_date
|
||||||
|
|
||||||
|
# Line-level timing flag = the worst-urgency row of the line's CURRENT
|
||||||
|
# shipment group (twin rows can differ; worst wins, fail-safe).
|
||||||
|
flag_by_group: dict = {}
|
||||||
|
for r in results:
|
||||||
|
gk = (r.patient_id, r.device_type, _as_date(r.last_shipment_date))
|
||||||
|
flag_val = r.flag.value if hasattr(r.flag, "value") else str(r.flag)
|
||||||
|
prev = flag_by_group.get(gk)
|
||||||
|
if prev is None or r.priority_score > prev[1]:
|
||||||
|
flag_by_group[gk] = (flag_val, r.priority_score)
|
||||||
|
|
||||||
|
# DISPLAY ordering, intentionally different from rollup.SEVERITY: the
|
||||||
|
# worklist queues At Risk above Plan Type Needed (urgency), while the
|
||||||
|
# rollup's capping rule treats PTN as the worst LINE state. Do not unify.
|
||||||
|
severity = {
|
||||||
|
"At Risk": 4,
|
||||||
|
"Plan Type Needed": 3,
|
||||||
|
"Action Needed": 2,
|
||||||
|
"On Track": 1,
|
||||||
|
"Clear to Ship": 0,
|
||||||
|
}
|
||||||
|
patients = []
|
||||||
|
for pid, ru in rollup_patients(reachable).items():
|
||||||
|
lines_out = []
|
||||||
|
for line in sorted(
|
||||||
|
ru.lines,
|
||||||
|
key=lambda l: (
|
||||||
|
-(severity.get(l.line_status, -1)),
|
||||||
|
l.device_type,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
line_key = (line.patient_id, line.device_type, line.plan_type)
|
||||||
|
verdict = index.by_line.get(line_key)
|
||||||
|
cur = line.current_shipment
|
||||||
|
timing = flag_by_group.get(
|
||||||
|
(line.patient_id, line.device_type, _as_date(cur.shipment_date))
|
||||||
|
)
|
||||||
|
lines_out.append(
|
||||||
|
LineOut(
|
||||||
|
device_type=line.device_type,
|
||||||
|
device_display=DEVICE_DISPLAY.get(
|
||||||
|
line.device_type, line.device_type
|
||||||
|
),
|
||||||
|
plan_type=None
|
||||||
|
if line.plan_type == UNKNOWN_PLAN_TYPE
|
||||||
|
else line.plan_type,
|
||||||
|
gradeable=line.gradeable,
|
||||||
|
line_status=line.line_status,
|
||||||
|
timing_flag=timing[0] if timing else None,
|
||||||
|
doc_items=_readiness_items_out(verdict),
|
||||||
|
order_numbers=cur.order_numbers,
|
||||||
|
hcpcs_codes=cur.hcpcs_codes,
|
||||||
|
total_quantity=cur.total_quantity,
|
||||||
|
shipment_date=cur.shipment_date.isoformat(),
|
||||||
|
payer=cur.payer or None,
|
||||||
|
prior_shipment_count=len(line.prior_shipments),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
patients.append(
|
||||||
|
PatientOut(
|
||||||
|
patient_id=pid,
|
||||||
|
rollup_status=ru.rollup_status.value
|
||||||
|
if ru.rollup_status
|
||||||
|
else None,
|
||||||
|
plan_type_needed=ru.plan_type_needed,
|
||||||
|
cgm_line_count=ru.cgm_line_count,
|
||||||
|
total_line_count=ru.total_line_count,
|
||||||
|
lines=lines_out,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Worklist order: worst patients first, stable by patient_id.
|
||||||
|
patients.sort(
|
||||||
|
key=lambda p: (-(severity.get(p.rollup_status, -1)), p.patient_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
stats: dict = {"patients_total": len(patients)}
|
||||||
|
for p in patients:
|
||||||
|
key = p.rollup_status if p.rollup_status else "ungraded"
|
||||||
|
stats[key] = stats.get(key, 0) + 1
|
||||||
|
return patients, stats
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health():
|
def health():
|
||||||
return {"status": "ok", "service": "signal-api", "version": "1.0.0"}
|
return {"status": "ok", "service": "signal-api", "version": "1.0.0"}
|
||||||
|
|
@ -547,10 +688,13 @@ async def upload_csv(
|
||||||
|
|
||||||
out = []
|
out = []
|
||||||
used_lines = {}
|
used_lines = {}
|
||||||
|
reachable_line_keys = set()
|
||||||
for r in results:
|
for r in results:
|
||||||
verdict, line_key, merged = resolve_group(
|
verdict, line_key, merged = resolve_group(
|
||||||
index, r.patient_id, r.device_type, r.last_shipment_date
|
index, r.patient_id, r.device_type, r.last_shipment_date
|
||||||
)
|
)
|
||||||
|
if line_key is not None:
|
||||||
|
reachable_line_keys.add(line_key)
|
||||||
if verdict is not None and line_key is not None:
|
if verdict is not None and line_key is not None:
|
||||||
used_lines[line_key] = verdict
|
used_lines[line_key] = verdict
|
||||||
# Exact row match first (byte-identical legacy path). CoverageResult
|
# Exact row match first (byte-identical legacy path). CoverageResult
|
||||||
|
|
@ -606,6 +750,10 @@ async def upload_csv(
|
||||||
clerk_org_id=clerk_org_id,
|
clerk_org_id=clerk_org_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
patients_out, patient_stats = _build_patients_payload(
|
||||||
|
index, reachable_line_keys, results
|
||||||
|
)
|
||||||
|
|
||||||
return UploadResponse(
|
return UploadResponse(
|
||||||
records=out,
|
records=out,
|
||||||
total=len(out),
|
total=len(out),
|
||||||
|
|
@ -618,6 +766,8 @@ async def upload_csv(
|
||||||
# reconcile with the records beside them (a record calculate_batch
|
# reconcile with the records beside them (a record calculate_batch
|
||||||
# skipped cannot leave a phantom line in the counts).
|
# skipped cannot leave a phantom line in the counts).
|
||||||
readiness_stats=readiness_stats(used_lines),
|
readiness_stats=readiness_stats(used_lines),
|
||||||
|
patients=patients_out,
|
||||||
|
patient_stats=patient_stats,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
{
|
{
|
||||||
"_comment": "Wear-day rules by device type and payer. Updated 2026-06-07. Run quarterly review per compliance checklist.",
|
"_comment": "Wear-day rules by device type and payer. Updated 2026-07-07 (P2: plan-scoped keys wrapped with citation rule_ids). Run quarterly review per compliance checklist.",
|
||||||
|
"_rule_registry": {
|
||||||
|
"R001": "visit_renewal_days — visit renewal window in days (standard: 180)",
|
||||||
|
"R002": "refill_window_days — resupply window in days before coverage end (standard: 30; legacy code key name retained)",
|
||||||
|
"R003": "pa_required — prior authorization required for this plan type",
|
||||||
|
"R004": "pecos_required — PECOS enrollment verification required for this plan type",
|
||||||
|
"R100": "swo_required — SWO is a universal claim requirement per LCD L33822",
|
||||||
|
"R101": "visit_required — qualifying visit is a universal requirement per LCD L33822",
|
||||||
|
"R102": "diagnosis_required — diagnosis (ICD-10) is a universal requirement per LCD L33822"
|
||||||
|
},
|
||||||
"devices": {
|
"devices": {
|
||||||
"dexcom_g6": {
|
"dexcom_g6": {
|
||||||
"display_name": "Dexcom G6",
|
"display_name": "Dexcom G6",
|
||||||
|
|
@ -31,42 +40,42 @@
|
||||||
},
|
},
|
||||||
"payer_rules": {
|
"payer_rules": {
|
||||||
"medicare": {
|
"medicare": {
|
||||||
"visit_renewal_days": 180,
|
"visit_renewal_days": { "value": 180, "rule_id": "R001" },
|
||||||
"refill_window_days": 30,
|
"refill_window_days": { "value": 30, "rule_id": "R002" },
|
||||||
"pa_required": false,
|
"pa_required": { "value": false, "rule_id": "R003" },
|
||||||
"pecos_required": true,
|
"pecos_required": { "value": true, "rule_id": "R004" },
|
||||||
"_note": "Medicare FFS. PA not required (CGM coverage expansion 2023). Face-to-face visit required every 6 months. CMS-1828-F: suppliers at >=90% affirmation rate may qualify for PA exemption from June 1, 2026.",
|
"_note": "Medicare FFS. PA not required (CGM coverage expansion 2023). Face-to-face visit required every 6 months. CMS-1828-F: suppliers at >=90% affirmation rate may qualify for PA exemption from June 1, 2026.",
|
||||||
"covered_devices": ["dexcom_g6", "dexcom_g7", "freestyle_libre_2", "freestyle_libre_3"]
|
"covered_devices": ["dexcom_g6", "dexcom_g7", "freestyle_libre_2", "freestyle_libre_3"]
|
||||||
},
|
},
|
||||||
"medicare_advantage": {
|
"medicare_advantage": {
|
||||||
"visit_renewal_days": 180,
|
"visit_renewal_days": { "value": 180, "rule_id": "R001" },
|
||||||
"refill_window_days": 30,
|
"refill_window_days": { "value": 30, "rule_id": "R002" },
|
||||||
"pa_required": true,
|
"pa_required": { "value": true, "rule_id": "R003" },
|
||||||
"pecos_required": true,
|
"pecos_required": { "value": true, "rule_id": "R004" },
|
||||||
"_note": "Medicare Advantage — PA required, varies by plan. Verify plan-specific rules.",
|
"_note": "Medicare Advantage — PA required, varies by plan. Verify plan-specific rules.",
|
||||||
"covered_devices": ["dexcom_g6", "dexcom_g7", "freestyle_libre_2", "freestyle_libre_3"]
|
"covered_devices": ["dexcom_g6", "dexcom_g7", "freestyle_libre_2", "freestyle_libre_3"]
|
||||||
},
|
},
|
||||||
"medicaid": {
|
"medicaid": {
|
||||||
"visit_renewal_days": 180,
|
"visit_renewal_days": { "value": 180, "rule_id": "R001" },
|
||||||
"refill_window_days": 30,
|
"refill_window_days": { "value": 30, "rule_id": "R002" },
|
||||||
"pa_required": true,
|
"pa_required": { "value": true, "rule_id": "R003" },
|
||||||
"pecos_required": false,
|
"pecos_required": { "value": false, "rule_id": "R004" },
|
||||||
"_note": "PA required — Keystone First, UPMC Health Plan, Highmark BCBS require PA. NJ FamilyCare, Horizon BCBS NJ require PA.",
|
"_note": "PA required — Keystone First, UPMC Health Plan, Highmark BCBS require PA. NJ FamilyCare, Horizon BCBS NJ require PA.",
|
||||||
"covered_devices": []
|
"covered_devices": []
|
||||||
},
|
},
|
||||||
"commercial": {
|
"commercial": {
|
||||||
"visit_renewal_days": 180,
|
"visit_renewal_days": { "value": 180, "rule_id": "R001" },
|
||||||
"refill_window_days": 30,
|
"refill_window_days": { "value": 30, "rule_id": "R002" },
|
||||||
"pa_required": true,
|
"pa_required": { "value": true, "rule_id": "R003" },
|
||||||
"pecos_required": false,
|
"pecos_required": { "value": false, "rule_id": "R004" },
|
||||||
"_note": "Commercial payer rules vary by plan. PA required by default — verify per plan.",
|
"_note": "Commercial payer rules vary by plan. PA required by default — verify per plan.",
|
||||||
"covered_devices": []
|
"covered_devices": []
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
"visit_renewal_days": 180,
|
"visit_renewal_days": { "value": 180, "rule_id": "R001" },
|
||||||
"refill_window_days": 30,
|
"refill_window_days": { "value": 30, "rule_id": "R002" },
|
||||||
"pa_required": true,
|
"pa_required": { "value": true, "rule_id": "R003" },
|
||||||
"pecos_required": false,
|
"pecos_required": { "value": false, "rule_id": "R004" },
|
||||||
"covered_devices": []
|
"covered_devices": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -328,8 +328,10 @@ def calculate_coverage(
|
||||||
rule_version=RULE_VERSION,
|
rule_version=RULE_VERSION,
|
||||||
)
|
)
|
||||||
|
|
||||||
visit_renewal_days = payer_config.get("visit_renewal_days")
|
from core.rules import rule_value
|
||||||
refill_window_days = payer_config.get("refill_window_days", 30)
|
|
||||||
|
visit_renewal_days = rule_value(payer_config, "visit_renewal_days")
|
||||||
|
refill_window_days = rule_value(payer_config, "refill_window_days", 30)
|
||||||
|
|
||||||
# Resolve visit date using priority chain
|
# Resolve visit date using priority chain
|
||||||
visit_date, confidence = _resolve_visit_date(record, confirmed_visit_date)
|
visit_date, confidence = _resolve_visit_date(record, confirmed_visit_date)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from core.rules import UNIVERSAL_RULE_IDS, rule_id_for, rule_value
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
PAYER_RULES_PATH = Path(__file__).parent.parent / "config" / "payer_rules.json"
|
PAYER_RULES_PATH = Path(__file__).parent.parent / "config" / "payer_rules.json"
|
||||||
|
|
@ -87,6 +89,9 @@ class DocItem:
|
||||||
quality: Quality
|
quality: Quality
|
||||||
value: Optional[str]
|
value: Optional[str]
|
||||||
satisfied: bool
|
satisfied: bool
|
||||||
|
# Names the payer_rules.json rule that decided required_state. None when
|
||||||
|
# required_state is NOT_EVALUATED: no rule fired, so nothing is cited.
|
||||||
|
rule_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -174,7 +179,8 @@ def _pa_quality(value: Optional[str]) -> Quality:
|
||||||
|
|
||||||
|
|
||||||
def _make_item(doc_type: str, required: RequiredState,
|
def _make_item(doc_type: str, required: RequiredState,
|
||||||
quality: Quality, value: Optional[str]) -> DocItem:
|
quality: Quality, value: Optional[str],
|
||||||
|
rule_id: Optional[str] = None) -> DocItem:
|
||||||
input_state = InputState.ABSENT if quality == Quality.NONE else InputState.SUPPLIED
|
input_state = InputState.ABSENT if quality == Quality.NONE else InputState.SUPPLIED
|
||||||
|
|
||||||
if required == RequiredState.NOT_REQUIRED:
|
if required == RequiredState.NOT_REQUIRED:
|
||||||
|
|
@ -191,26 +197,36 @@ def _make_item(doc_type: str, required: RequiredState,
|
||||||
quality=quality,
|
quality=quality,
|
||||||
value=value,
|
value=value,
|
||||||
satisfied=satisfied,
|
satisfied=satisfied,
|
||||||
|
rule_id=rule_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _required_from_config(plan_known: bool, plan_cfg: Optional[dict],
|
def _required_from_config(
|
||||||
flag_key: str, plan_type: Optional[str]) -> RequiredState:
|
plan_known: bool, plan_cfg: Optional[dict],
|
||||||
"""Required-ness for a plan-dependent item, read from config.
|
flag_key: str, plan_type: Optional[str],
|
||||||
|
) -> tuple[RequiredState, Optional[str]]:
|
||||||
|
"""(Required-ness, citing rule_id) for a plan-dependent item, from config.
|
||||||
|
|
||||||
A missing config (known plan absent from payer_rules.json, or the flag key
|
A missing config (known plan absent from payer_rules.json, or the flag key
|
||||||
missing) means we CANNOT decide -> NOT_EVALUATED, which blocks green. We
|
missing) means we CANNOT decide -> NOT_EVALUATED, which blocks green. We
|
||||||
never treat a config gap as 'not required' (that was the false-green hole).
|
never treat a config gap as 'not required' (that was the false-green hole).
|
||||||
|
When NOT_EVALUATED, rule_id is None: no rule fired, nothing is cited.
|
||||||
|
Accepts both flat and wrapped ({"value":..., "rule_id":...}) config values.
|
||||||
"""
|
"""
|
||||||
if not plan_known:
|
if not plan_known:
|
||||||
return RequiredState.NOT_EVALUATED
|
return RequiredState.NOT_EVALUATED, None
|
||||||
if not plan_cfg or flag_key not in plan_cfg:
|
if not plan_cfg or flag_key not in plan_cfg:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"plan_type '%s' missing '%s' in payer_rules.json -> cannot grade %s",
|
"plan_type '%s' missing '%s' in payer_rules.json -> cannot grade %s",
|
||||||
plan_type, flag_key, flag_key,
|
plan_type, flag_key, flag_key,
|
||||||
)
|
)
|
||||||
return RequiredState.NOT_EVALUATED
|
return RequiredState.NOT_EVALUATED, None
|
||||||
return RequiredState.REQUIRED if bool(plan_cfg[flag_key]) else RequiredState.NOT_REQUIRED
|
required = (
|
||||||
|
RequiredState.REQUIRED
|
||||||
|
if bool(rule_value(plan_cfg, flag_key))
|
||||||
|
else RequiredState.NOT_REQUIRED
|
||||||
|
)
|
||||||
|
return required, rule_id_for(plan_cfg, flag_key)
|
||||||
|
|
||||||
|
|
||||||
def evaluate_readiness(
|
def evaluate_readiness(
|
||||||
|
|
@ -233,19 +249,24 @@ def evaluate_readiness(
|
||||||
|
|
||||||
# Plan-dependent required-ness from config (single source of truth, never sets).
|
# Plan-dependent required-ness from config (single source of truth, never sets).
|
||||||
plan_cfg = rules.get("payer_rules", {}).get(plan_type) if plan_known else None
|
plan_cfg = rules.get("payer_rules", {}).get(plan_type) if plan_known else None
|
||||||
pecos_required = _required_from_config(plan_known, plan_cfg, "pecos_required", plan_type)
|
pecos_required, pecos_rule = _required_from_config(plan_known, plan_cfg, "pecos_required", plan_type)
|
||||||
pa_required = _required_from_config(plan_known, plan_cfg, "pa_required", plan_type)
|
pa_required, pa_rule = _required_from_config(plan_known, plan_cfg, "pa_required", plan_type)
|
||||||
|
|
||||||
visit_date = csv_visit_date or confirmed_visit_date
|
visit_date = csv_visit_date or confirmed_visit_date
|
||||||
items = [
|
items = [
|
||||||
_make_item("swo", RequiredState.REQUIRED, _swo_quality(csv_swo_status), csv_swo_status),
|
_make_item("swo", RequiredState.REQUIRED, _swo_quality(csv_swo_status),
|
||||||
|
csv_swo_status, rule_id=UNIVERSAL_RULE_IDS["swo"]),
|
||||||
_make_item("visit", RequiredState.REQUIRED,
|
_make_item("visit", RequiredState.REQUIRED,
|
||||||
_visit_quality(csv_visit_date, confirmed_visit_date),
|
_visit_quality(csv_visit_date, confirmed_visit_date),
|
||||||
visit_date.isoformat() if visit_date else None),
|
visit_date.isoformat() if visit_date else None,
|
||||||
|
rule_id=UNIVERSAL_RULE_IDS["visit"]),
|
||||||
_make_item("diagnosis", RequiredState.REQUIRED,
|
_make_item("diagnosis", RequiredState.REQUIRED,
|
||||||
_diagnosis_quality(csv_diagnosis_on_file), csv_diagnosis_on_file),
|
_diagnosis_quality(csv_diagnosis_on_file), csv_diagnosis_on_file,
|
||||||
_make_item("pecos", pecos_required, _pecos_quality(csv_pecos_verified), csv_pecos_verified),
|
rule_id=UNIVERSAL_RULE_IDS["diagnosis"]),
|
||||||
_make_item("pa", pa_required, _pa_quality(csv_pa_status), csv_pa_status),
|
_make_item("pecos", pecos_required, _pecos_quality(csv_pecos_verified),
|
||||||
|
csv_pecos_verified, rule_id=pecos_rule),
|
||||||
|
_make_item("pa", pa_required, _pa_quality(csv_pa_status),
|
||||||
|
csv_pa_status, rule_id=pa_rule),
|
||||||
]
|
]
|
||||||
|
|
||||||
return ReadinessVerdict(
|
return ReadinessVerdict(
|
||||||
|
|
|
||||||
101
python-backend/core/rollup.py
Normal file
101
python-backend/core/rollup.py
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
"""
|
||||||
|
rollup.py
|
||||||
|
Signal — STTIL Solutions
|
||||||
|
|
||||||
|
Patient-level rollup of coverage-line readiness verdicts (build plan 02, P3).
|
||||||
|
Design locked in readiness-model-brief section 4 and the phase-2 design Item 3:
|
||||||
|
|
||||||
|
- The light = worst gradeable line. Severity (worst first):
|
||||||
|
Plan Type Needed > At Risk > Action Needed > On Track > Clear to Ship.
|
||||||
|
- Any gradeable Plan Type Needed line caps the patient no greener than
|
||||||
|
Action Needed (the patient IS being served; one unmapped plan type must not
|
||||||
|
read as the whole patient being ungradeable).
|
||||||
|
- Non-gradeable lines (non-CGM) never participate in the light or the counts.
|
||||||
|
- Tabs count patients, not rows.
|
||||||
|
|
||||||
|
A patient with no graded gradeable lines has rollup_status None — the UI
|
||||||
|
treatment is an open Kisa question; no label is invented here.
|
||||||
|
|
||||||
|
PHI CONTRACT: operates on patient_id only; this module logs nothing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from core.dedup import CoverageLine
|
||||||
|
from core.readiness import LineStatus
|
||||||
|
|
||||||
|
# Higher = worse. Plan Type Needed ranks worst for LINE severity ordering;
|
||||||
|
# its patient-level effect is the capping rule below, not raw worstness.
|
||||||
|
SEVERITY = {
|
||||||
|
LineStatus.CLEAR_TO_SHIP: 0,
|
||||||
|
LineStatus.ON_TRACK: 1,
|
||||||
|
LineStatus.ACTION_NEEDED: 2,
|
||||||
|
LineStatus.AT_RISK: 3,
|
||||||
|
LineStatus.PLAN_TYPE_NEEDED: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PatientRollup:
|
||||||
|
patient_id: str
|
||||||
|
lines: list[CoverageLine] # all lines, incl. non-gradeable
|
||||||
|
rollup_status: Optional[LineStatus] # None = no graded gradeable lines
|
||||||
|
cgm_line_count: int # gradeable lines
|
||||||
|
total_line_count: int # all lines
|
||||||
|
plan_type_needed: bool # any gradeable line in PTN
|
||||||
|
|
||||||
|
|
||||||
|
def rollup_patient(lines: list[CoverageLine]) -> PatientRollup:
|
||||||
|
"""Roll one patient's coverage lines into a single verdict, per the
|
||||||
|
locked truth table (plan 02 P3):
|
||||||
|
|
||||||
|
| gradeable statuses present | rollup_status |
|
||||||
|
| none | None |
|
||||||
|
| all Plan Type Needed | Plan Type Needed |
|
||||||
|
| some PTN + others | worse of (Action Needed, worst other)|
|
||||||
|
| no PTN | worst by severity |
|
||||||
|
"""
|
||||||
|
if not lines:
|
||||||
|
raise ValueError("rollup_patient requires at least one CoverageLine")
|
||||||
|
graded = [
|
||||||
|
l for l in lines if l.gradeable and l.line_status is not None
|
||||||
|
]
|
||||||
|
statuses = [LineStatus(l.line_status) for l in graded]
|
||||||
|
ptn = [s for s in statuses if s == LineStatus.PLAN_TYPE_NEEDED]
|
||||||
|
|
||||||
|
if not statuses:
|
||||||
|
status: Optional[LineStatus] = None
|
||||||
|
elif len(ptn) == len(statuses):
|
||||||
|
status = LineStatus.PLAN_TYPE_NEEDED
|
||||||
|
elif ptn:
|
||||||
|
worst_other = max(
|
||||||
|
(s for s in statuses if s != LineStatus.PLAN_TYPE_NEEDED),
|
||||||
|
key=SEVERITY.get,
|
||||||
|
)
|
||||||
|
status = max(worst_other, LineStatus.ACTION_NEEDED, key=SEVERITY.get)
|
||||||
|
else:
|
||||||
|
status = max(statuses, key=SEVERITY.get)
|
||||||
|
|
||||||
|
return PatientRollup(
|
||||||
|
patient_id=lines[0].patient_id,
|
||||||
|
lines=lines,
|
||||||
|
rollup_status=status,
|
||||||
|
cgm_line_count=sum(1 for l in lines if l.gradeable),
|
||||||
|
total_line_count=len(lines),
|
||||||
|
plan_type_needed=bool(ptn),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rollup_patients(lines: dict) -> dict[str, PatientRollup]:
|
||||||
|
"""Group CoverageLines (any keying) by patient and roll each up."""
|
||||||
|
by_patient: dict[str, list[CoverageLine]] = defaultdict(list)
|
||||||
|
for line in lines.values():
|
||||||
|
by_patient[line.patient_id].append(line)
|
||||||
|
return {
|
||||||
|
patient_id: rollup_patient(patient_lines)
|
||||||
|
for patient_id, patient_lines in by_patient.items()
|
||||||
|
}
|
||||||
52
python-backend/core/rules.py
Normal file
52
python-backend/core/rules.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""
|
||||||
|
rules.py
|
||||||
|
Signal — STTIL Solutions
|
||||||
|
|
||||||
|
Format-tolerant readers for payer_rules.json plus the stable rule-ID registry
|
||||||
|
(build plan 02, item P2; design: readiness-model-design-phase2 section 3.1).
|
||||||
|
|
||||||
|
A rule value may be FLAT (legacy: `"visit_renewal_days": 180`) or WRAPPED with
|
||||||
|
its citation (`"visit_renewal_days": {"value": 180, "rule_id": "R001"}`).
|
||||||
|
Both engines read through these helpers so the JSON format can carry citation
|
||||||
|
IDs without breaking either reader.
|
||||||
|
|
||||||
|
RULE IDS ARE STABLE — never change an ID after shipping. The ID names the rule
|
||||||
|
KIND; the plan type plus the value together are the citation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# Universal claim/coverage requirements per LCD L33822, independent of plan
|
||||||
|
# type. They live in code (no plan-type dependency) but carry rule IDs for
|
||||||
|
# citation consistency.
|
||||||
|
UNIVERSAL_RULE_IDS = {"swo": "R100", "visit": "R101", "diagnosis": "R102"}
|
||||||
|
|
||||||
|
# Plan-scoped keys in payer_rules.json and their registry IDs.
|
||||||
|
PLAN_KEY_RULE_IDS = {
|
||||||
|
"visit_renewal_days": "R001",
|
||||||
|
"refill_window_days": "R002",
|
||||||
|
"pa_required": "R003",
|
||||||
|
"pecos_required": "R004",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def rule_value(cfg: Optional[dict], key: str, default=None):
|
||||||
|
"""Read a rule value, accepting flat (180) and wrapped
|
||||||
|
({'value': 180, 'rule_id': 'R001'}) formats."""
|
||||||
|
if not cfg:
|
||||||
|
return default
|
||||||
|
raw = cfg.get(key, default)
|
||||||
|
if isinstance(raw, dict) and "value" in raw:
|
||||||
|
return raw["value"]
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def rule_id_for(cfg: Optional[dict], key: str) -> Optional[str]:
|
||||||
|
"""The citation ID for a plan-scoped rule: from the wrapped config when
|
||||||
|
present, else the registry default for that key."""
|
||||||
|
raw = cfg.get(key) if cfg else None
|
||||||
|
if isinstance(raw, dict) and "rule_id" in raw:
|
||||||
|
return raw["rule_id"]
|
||||||
|
return PLAN_KEY_RULE_IDS.get(key)
|
||||||
|
|
@ -36,6 +36,7 @@ from typing import Optional
|
||||||
from core.coverage_calculator import ShipmentRecord
|
from core.coverage_calculator import ShipmentRecord
|
||||||
from core.dedup import (
|
from core.dedup import (
|
||||||
UNKNOWN_PLAN_TYPE,
|
UNKNOWN_PLAN_TYPE,
|
||||||
|
CoverageLine,
|
||||||
MergedShipment,
|
MergedShipment,
|
||||||
_as_date,
|
_as_date,
|
||||||
assign_to_coverage_lines,
|
assign_to_coverage_lines,
|
||||||
|
|
@ -54,11 +55,13 @@ class ReadinessIndex:
|
||||||
"""Dedup-derived resolution index for one upload batch.
|
"""Dedup-derived resolution index for one upload batch.
|
||||||
|
|
||||||
by_line: verdicts per gradeable coverage line
|
by_line: verdicts per gradeable coverage line
|
||||||
|
lines: every CoverageLine (graded ones carry line_status)
|
||||||
line_by_group: dedup group -> the line that group's shipment belongs to
|
line_by_group: dedup group -> the line that group's shipment belongs to
|
||||||
merged_by_group: dedup group -> its MergedShipment (display-field truth)
|
merged_by_group: dedup group -> its MergedShipment (display-field truth)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
by_line: dict[LineKey, ReadinessVerdict] = field(default_factory=dict)
|
by_line: dict[LineKey, ReadinessVerdict] = field(default_factory=dict)
|
||||||
|
lines: dict[LineKey, "CoverageLine"] = field(default_factory=dict)
|
||||||
line_by_group: dict[GroupKey, LineKey] = field(default_factory=dict)
|
line_by_group: dict[GroupKey, LineKey] = field(default_factory=dict)
|
||||||
merged_by_group: dict[GroupKey, MergedShipment] = field(default_factory=dict)
|
merged_by_group: dict[GroupKey, MergedShipment] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
@ -93,22 +96,26 @@ def build_readiness_index(
|
||||||
|
|
||||||
index = ReadinessIndex()
|
index = ReadinessIndex()
|
||||||
for key, line in lines.items():
|
for key, line in lines.items():
|
||||||
|
line_key = (key.patient_id, key.device_type, key.plan_type_id)
|
||||||
|
index.lines[line_key] = line
|
||||||
if not line.gradeable:
|
if not line.gradeable:
|
||||||
continue
|
continue
|
||||||
cur = line.current_shipment
|
cur = line.current_shipment
|
||||||
index.by_line[(key.patient_id, key.device_type, key.plan_type_id)] = (
|
verdict = evaluate_readiness(
|
||||||
evaluate_readiness(
|
line.plan_type if line.plan_type != UNKNOWN_PLAN_TYPE else None,
|
||||||
line.plan_type if line.plan_type != UNKNOWN_PLAN_TYPE else None,
|
csv_swo_status=cur.csv_swo_status,
|
||||||
csv_swo_status=cur.csv_swo_status,
|
csv_visit_date=cur.csv_visit_date,
|
||||||
csv_visit_date=cur.csv_visit_date,
|
confirmed_visit_date=_confirmed_for(
|
||||||
confirmed_visit_date=_confirmed_for(
|
key.patient_id, confirmed_visits
|
||||||
key.patient_id, confirmed_visits
|
),
|
||||||
),
|
csv_pecos_verified=cur.csv_pecos_verified,
|
||||||
csv_pecos_verified=cur.csv_pecos_verified,
|
csv_pa_status=cur.csv_pa_status,
|
||||||
csv_pa_status=cur.csv_pa_status,
|
csv_diagnosis_on_file=cur.csv_diagnosis_on_file,
|
||||||
csv_diagnosis_on_file=cur.csv_diagnosis_on_file,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
index.by_line[line_key] = verdict
|
||||||
|
# The grading step owns line_status (dedup leaves it None) — the
|
||||||
|
# rollup (P3) reads it from the line.
|
||||||
|
line.line_status = verdict.line_status.value
|
||||||
|
|
||||||
for m in merged:
|
for m in merged:
|
||||||
gkey: GroupKey = (m.patient_id, m.device_type, m.shipment_date)
|
gkey: GroupKey = (m.patient_id, m.device_type, m.shipment_date)
|
||||||
|
|
|
||||||
|
|
@ -101,3 +101,12 @@ def test_priority_sort_order():
|
||||||
assert results[0].flag == CoverageFlag.SUPPLY_LAPSED
|
assert results[0].flag == CoverageFlag.SUPPLY_LAPSED
|
||||||
assert results[1].flag == CoverageFlag.RENEWAL_CRITICAL
|
assert results[1].flag == CoverageFlag.RENEWAL_CRITICAL
|
||||||
assert results[2].flag == CoverageFlag.ACTIVE
|
assert results[2].flag == CoverageFlag.ACTIVE
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrapped_config_drives_timing_rules():
|
||||||
|
"""P2: with the wrapped citation format on disk, the timing calculator
|
||||||
|
still resolves visit_renewal_days correctly (R001 = 180 days)."""
|
||||||
|
visit = TODAY - timedelta(days=30)
|
||||||
|
r = make_record(csv_visit_date=visit)
|
||||||
|
result = calculate_coverage(r)
|
||||||
|
assert result.next_visit_due_date == visit + timedelta(days=180)
|
||||||
|
|
|
||||||
|
|
@ -255,3 +255,70 @@ def test_known_plan_types_match_payer_rules_config():
|
||||||
configured = set(json.loads(rules_path.read_text())["payer_rules"].keys()) - {"default"}
|
configured = set(json.loads(rules_path.read_text())["payer_rules"].keys()) - {"default"}
|
||||||
from core.readiness import KNOWN_PLAN_TYPES
|
from core.readiness import KNOWN_PLAN_TYPES
|
||||||
assert set(KNOWN_PLAN_TYPES) == configured
|
assert set(KNOWN_PLAN_TYPES) == configured
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# P2: citation rule IDs (build plan 02, readiness-model-design-phase2 §3.1)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_doc_items_carry_rule_ids():
|
||||||
|
v = evaluate_readiness(
|
||||||
|
"medicare",
|
||||||
|
csv_swo_status="On File",
|
||||||
|
csv_visit_date=VISIT,
|
||||||
|
csv_pecos_verified="Yes",
|
||||||
|
csv_diagnosis_on_file="Yes",
|
||||||
|
)
|
||||||
|
items = items_by_type(v)
|
||||||
|
assert items["swo"].rule_id == "R100"
|
||||||
|
assert items["visit"].rule_id == "R101"
|
||||||
|
assert items["diagnosis"].rule_id == "R102"
|
||||||
|
assert items["pecos"].rule_id == "R004"
|
||||||
|
assert items["pa"].rule_id == "R003"
|
||||||
|
|
||||||
|
|
||||||
|
def test_flat_and_wrapped_config_produce_identical_verdicts():
|
||||||
|
flat = {"payer_rules": {"medicare": {
|
||||||
|
"pa_required": False, "pecos_required": True,
|
||||||
|
}}}
|
||||||
|
wrapped = {"payer_rules": {"medicare": {
|
||||||
|
"pa_required": {"value": False, "rule_id": "R003"},
|
||||||
|
"pecos_required": {"value": True, "rule_id": "R004"},
|
||||||
|
}}}
|
||||||
|
kwargs = dict(
|
||||||
|
csv_swo_status="On File", csv_visit_date=VISIT,
|
||||||
|
csv_pecos_verified="Yes", csv_diagnosis_on_file="Yes",
|
||||||
|
)
|
||||||
|
v_flat = evaluate_readiness("medicare", rules=flat, **kwargs)
|
||||||
|
v_wrapped = evaluate_readiness("medicare", rules=wrapped, **kwargs)
|
||||||
|
assert v_flat.line_status == v_wrapped.line_status
|
||||||
|
for a, b in zip(v_flat.doc_items, v_wrapped.doc_items):
|
||||||
|
assert (a.doc_type, a.required_state, a.satisfied) == (
|
||||||
|
b.doc_type, b.required_state, b.satisfied
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_not_evaluated_items_have_no_rule_id():
|
||||||
|
v = evaluate_readiness(None)
|
||||||
|
items = items_by_type(v)
|
||||||
|
assert items["pecos"].required_state == RequiredState.NOT_EVALUATED
|
||||||
|
assert items["pecos"].rule_id is None
|
||||||
|
assert items["pa"].rule_id is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_on_disk_rule_ids_match_registry():
|
||||||
|
"""Guards citation drift: every wrapped rule_id in payer_rules.json must
|
||||||
|
equal the registry ID for its key (auditor finding L2, 2026-07-07)."""
|
||||||
|
import json
|
||||||
|
from pathlib import Path as _P
|
||||||
|
from core.rules import PLAN_KEY_RULE_IDS
|
||||||
|
|
||||||
|
cfg = json.loads(
|
||||||
|
(_P(__file__).parent.parent / "python-backend" / "config" / "payer_rules.json").read_text()
|
||||||
|
)
|
||||||
|
for plan, entry in cfg["payer_rules"].items():
|
||||||
|
for key, expected_id in PLAN_KEY_RULE_IDS.items():
|
||||||
|
raw = entry.get(key)
|
||||||
|
assert isinstance(raw, dict) and raw.get("rule_id") == expected_id, (
|
||||||
|
f"{plan}.{key} must be wrapped with rule_id {expected_id}"
|
||||||
|
)
|
||||||
|
|
|
||||||
238
tests/test_rollup.py
Normal file
238
tests/test_rollup.py
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
"""
|
||||||
|
Tests for the patient rollup (build plan 02, P3).
|
||||||
|
|
||||||
|
Rollup rules locked in readiness-model-brief section 4 / phase-2 design Item 3:
|
||||||
|
worst gradeable line; any gradeable Plan Type Needed line caps the patient no
|
||||||
|
greener than Action Needed; non-gradeable lines never touch the light; a
|
||||||
|
patient with no graded gradeable lines has rollup_status None (no label
|
||||||
|
invented).
|
||||||
|
|
||||||
|
PHI CONTRACT: synthetic patient_ids only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent / "python-backend"))
|
||||||
|
|
||||||
|
from core.dedup import CoverageLine, MergedShipment # noqa: E402
|
||||||
|
from core.readiness import LineStatus # noqa: E402
|
||||||
|
from core.rollup import rollup_patient, rollup_patients # noqa: E402
|
||||||
|
|
||||||
|
TODAY = date.today()
|
||||||
|
|
||||||
|
|
||||||
|
def make_line(patient_id="PT-1", device="dexcom_g7", plan="medicare",
|
||||||
|
status=None, gradeable=True):
|
||||||
|
shipment = MergedShipment(
|
||||||
|
patient_id=patient_id,
|
||||||
|
device_type=device,
|
||||||
|
shipment_date=TODAY - timedelta(days=5),
|
||||||
|
total_quantity=3,
|
||||||
|
components=["sensor"],
|
||||||
|
order_numbers=[],
|
||||||
|
hcpcs_codes=[],
|
||||||
|
payer="Medicare Part B",
|
||||||
|
plan_type=None if plan == "unknown" else plan,
|
||||||
|
)
|
||||||
|
return CoverageLine(
|
||||||
|
patient_id=patient_id,
|
||||||
|
device_type=device,
|
||||||
|
plan_type=plan,
|
||||||
|
gradeable=gradeable,
|
||||||
|
current_shipment=shipment,
|
||||||
|
prior_shipments=[],
|
||||||
|
line_status=status,
|
||||||
|
timing_flag=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_rollup_single_line():
|
||||||
|
ru = rollup_patient([make_line(status="On Track")])
|
||||||
|
assert ru.rollup_status == LineStatus.ON_TRACK
|
||||||
|
assert ru.cgm_line_count == 1
|
||||||
|
assert ru.total_line_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_rollup_mixed_statuses():
|
||||||
|
ru = rollup_patient([
|
||||||
|
make_line(device="dexcom_g7", status="Clear to Ship"),
|
||||||
|
make_line(device="freestyle_libre_2", status="At Risk"),
|
||||||
|
])
|
||||||
|
assert ru.rollup_status == LineStatus.AT_RISK
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_rollup_plan_type_needed_capping():
|
||||||
|
# PTN + Clear to Ship = Action Needed (capped, not PTN)
|
||||||
|
ru = rollup_patient([
|
||||||
|
make_line(device="dexcom_g7", status="Clear to Ship"),
|
||||||
|
make_line(device="freestyle_libre_2", plan="unknown",
|
||||||
|
status="Plan Type Needed"),
|
||||||
|
])
|
||||||
|
assert ru.rollup_status == LineStatus.ACTION_NEEDED
|
||||||
|
assert ru.plan_type_needed is True
|
||||||
|
# PTN + At Risk = At Risk (the worse of the two)
|
||||||
|
ru2 = rollup_patient([
|
||||||
|
make_line(device="dexcom_g7", status="At Risk"),
|
||||||
|
make_line(device="freestyle_libre_2", plan="unknown",
|
||||||
|
status="Plan Type Needed"),
|
||||||
|
])
|
||||||
|
assert ru2.rollup_status == LineStatus.AT_RISK
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_rollup_all_lines_plan_type_needed():
|
||||||
|
ru = rollup_patient([
|
||||||
|
make_line(device="dexcom_g7", plan="unknown", status="Plan Type Needed"),
|
||||||
|
make_line(device="freestyle_libre_2", plan="unknown",
|
||||||
|
status="Plan Type Needed"),
|
||||||
|
])
|
||||||
|
assert ru.rollup_status == LineStatus.PLAN_TYPE_NEEDED
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_rollup_non_cgm_excluded():
|
||||||
|
ru = rollup_patient([
|
||||||
|
make_line(device="dexcom_g7", status="Clear to Ship"),
|
||||||
|
make_line(device="omnipod_5", gradeable=False, status=None),
|
||||||
|
])
|
||||||
|
assert ru.rollup_status == LineStatus.CLEAR_TO_SHIP # pod changes nothing
|
||||||
|
assert ru.cgm_line_count == 1
|
||||||
|
assert ru.total_line_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_rollup_no_gradeable_lines():
|
||||||
|
ru = rollup_patient([make_line(device="omnipod_5", gradeable=False)])
|
||||||
|
assert ru.rollup_status is None
|
||||||
|
assert ru.total_line_count == 1
|
||||||
|
assert ru.cgm_line_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_rollup_patients_groups_by_patient():
|
||||||
|
lines = {
|
||||||
|
("A", "dexcom_g7", "medicare"): make_line("A", status="Clear to Ship"),
|
||||||
|
("A", "freestyle_libre_2", "medicare"): make_line(
|
||||||
|
"A", device="freestyle_libre_2", status="Action Needed"),
|
||||||
|
("B", "dexcom_g7", "unknown"): make_line(
|
||||||
|
"B", plan="unknown", status="Plan Type Needed"),
|
||||||
|
}
|
||||||
|
rollups = rollup_patients(lines)
|
||||||
|
assert set(rollups) == {"A", "B"}
|
||||||
|
assert rollups["A"].rollup_status == LineStatus.ACTION_NEEDED
|
||||||
|
assert rollups["B"].rollup_status == LineStatus.PLAN_TYPE_NEEDED
|
||||||
|
|
||||||
|
|
||||||
|
# --- API level ---------------------------------------------------------------
|
||||||
|
|
||||||
|
httpx = pytest.importorskip("httpx", reason="httpx required for TestClient")
|
||||||
|
from fastapi.testclient import TestClient # noqa: E402
|
||||||
|
|
||||||
|
import api.main as api_main # noqa: E402
|
||||||
|
|
||||||
|
client = TestClient(api_main.app)
|
||||||
|
|
||||||
|
SHIP = (TODAY - timedelta(days=5)).isoformat()
|
||||||
|
VISIT = (TODAY - timedelta(days=30)).isoformat()
|
||||||
|
HEADER = (
|
||||||
|
"patient_id,device_type,shipment_date,quantity,payer,plan_type,"
|
||||||
|
"swo_status,visit_date,pecos_verified,pa_status,diagnosis_on_file"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _upload(csv_text: str):
|
||||||
|
return client.post(
|
||||||
|
"/api/upload",
|
||||||
|
files={"file": ("fixture.csv", io.BytesIO(csv_text.encode()), "text/csv")},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_upload_response_contains_patients_nested():
|
||||||
|
csv_text = (
|
||||||
|
HEADER
|
||||||
|
+ f"\nPT-N-1,Dexcom G7,{SHIP},3,Medicare Part B,medicare,"
|
||||||
|
+ f"On File,{VISIT},Yes,,Yes"
|
||||||
|
+ f"\nPT-N-1,Libre 2,{SHIP},1,Medicare Part B,medicare,"
|
||||||
|
+ "On File,,Yes,,Yes"
|
||||||
|
)
|
||||||
|
resp = _upload(csv_text)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
patients = body["patients"]
|
||||||
|
assert len(patients) == 1
|
||||||
|
p = patients[0]
|
||||||
|
assert p["patient_id"] == "PT-N-1"
|
||||||
|
assert p["rollup_status"] == "Action Needed" # worst line (Libre 2 no visit)
|
||||||
|
assert len(p["lines"]) == 2
|
||||||
|
by_dev = {l["device_type"]: l for l in p["lines"]}
|
||||||
|
assert by_dev["dexcom_g7"]["line_status"] == "Clear to Ship"
|
||||||
|
assert by_dev["freestyle_libre_2"]["line_status"] == "Action Needed"
|
||||||
|
items = {i["doc_type"]: i for i in by_dev["freestyle_libre_2"]["doc_items"]}
|
||||||
|
assert items["visit"]["satisfied"] is False
|
||||||
|
assert items["visit"]["rule_id"] == "R101"
|
||||||
|
# Worst line renders first
|
||||||
|
assert p["lines"][0]["device_type"] == "freestyle_libre_2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_patient_stats_count_patients_not_rows():
|
||||||
|
csv_text = (
|
||||||
|
HEADER
|
||||||
|
+ f"\nPT-S-1,Dexcom G7,{SHIP},3,Medicare Part B,medicare,"
|
||||||
|
+ f"On File,{VISIT},Yes,,Yes"
|
||||||
|
+ f"\nPT-S-1,Libre 2,{SHIP},1,Medicare Part B,medicare,"
|
||||||
|
+ f"On File,{VISIT},Yes,,Yes"
|
||||||
|
+ f"\nPT-S-2,Dexcom G6,{SHIP},3,Medicaid - GA,,"
|
||||||
|
+ f"On File,{VISIT},,,Yes"
|
||||||
|
)
|
||||||
|
resp = _upload(csv_text)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
stats = resp.json()["patient_stats"]
|
||||||
|
assert stats["patients_total"] == 2 # 2-device patient counts once
|
||||||
|
assert stats["Clear to Ship"] == 1
|
||||||
|
assert stats["Plan Type Needed"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_flat_records_untouched_by_nested_payload():
|
||||||
|
csv_text = (
|
||||||
|
HEADER
|
||||||
|
+ f"\nPT-F-1,Dexcom G7,{SHIP},3,Medicare Part B,medicare,"
|
||||||
|
+ f"On File,{VISIT},Yes,,Yes"
|
||||||
|
)
|
||||||
|
resp = _upload(csv_text)
|
||||||
|
body = resp.json()
|
||||||
|
assert body["total"] == 1
|
||||||
|
assert len(body["records"]) == 1 # flat path byte-identical (P6 switches UI)
|
||||||
|
assert body["records"][0]["readiness_status"] == "Clear to Ship"
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_gradeable_only_patient_has_null_rollup():
|
||||||
|
csv_text = (
|
||||||
|
HEADER
|
||||||
|
+ f"\nPT-P-1,Omnipod 5,{SHIP},1,Highmark BCBS,commercial,"
|
||||||
|
+ f"On File,{VISIT},Yes,Approved,Yes"
|
||||||
|
)
|
||||||
|
resp = _upload(csv_text)
|
||||||
|
body = resp.json()
|
||||||
|
p = body["patients"][0]
|
||||||
|
assert p["rollup_status"] is None
|
||||||
|
assert p["cgm_line_count"] == 0
|
||||||
|
assert p["lines"][0]["gradeable"] is False
|
||||||
|
assert p["lines"][0]["line_status"] is None
|
||||||
|
assert body["patient_stats"]["ungraded"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_rollup_patient_empty_raises():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
rollup_patient([])
|
||||||
|
|
||||||
|
|
||||||
|
def test_line_timing_flag_populated():
|
||||||
|
csv_text = (
|
||||||
|
HEADER
|
||||||
|
+ f"\nPT-T-1,Dexcom G7,{SHIP},3,Medicare Part B,medicare,"
|
||||||
|
+ f"On File,{VISIT},Yes,,Yes"
|
||||||
|
)
|
||||||
|
resp = _upload(csv_text)
|
||||||
|
line = resp.json()["patients"][0]["lines"][0]
|
||||||
|
assert line["timing_flag"] == "RESUPPLY_READY"
|
||||||
|
|
@ -594,3 +594,18 @@ def test_confirm_visit_normalizes_plan_type(monkeypatch):
|
||||||
rec = resp.json()
|
rec = resp.json()
|
||||||
assert rec["plan_type"] == "medicare"
|
assert rec["plan_type"] == "medicare"
|
||||||
assert rec["readiness_status"] == "Clear to Ship"
|
assert rec["readiness_status"] == "Clear to Ship"
|
||||||
|
|
||||||
|
|
||||||
|
def test_upload_readiness_items_carry_rule_ids():
|
||||||
|
"""P2 contract W2: the serialized doc_items cite their rules."""
|
||||||
|
csv_text = (
|
||||||
|
HEADER
|
||||||
|
+ f"\nPT-RID-1,Dexcom G7,{SHIP},3,Medicare Part B,medicare,"
|
||||||
|
+ f"On File,{VISIT},Yes,,Yes"
|
||||||
|
)
|
||||||
|
resp = _upload(csv_text)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
items = {i["doc_type"]: i for i in resp.json()["records"][0]["readiness_items"]}
|
||||||
|
assert items["pecos"]["rule_id"] == "R004"
|
||||||
|
assert items["pa"]["rule_id"] == "R003"
|
||||||
|
assert items["swo"]["rule_id"] == "R100"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue