Add readiness-model dedup + coverage-line grouping foundation

Group normalized shipment rows into deduped shipments and gradeable
coverage lines (patient -> device -> shipment), the structural fix for the
row-keyed worklist bug (six months of orders no longer = six worklist lines).

- Locked dedup key (patient_id, device_type, date_of_service); order_number
  is display-only. Monthly resupplies stay separate, not duplicates.
- Client-mapped plan_type carried through, never guessed from payer name;
  conflicting plan types in a group route to "plan type needed".
- Quantity merge: sum across distinct component configs, max within a config
  (duplicate guard) with a warning breadcrumb on disagreement.
- CGM gradeable this phase; pumps preserved/displayed but not graded.

Additive only: not yet wired into the live pipeline, grading unchanged.
30 tests green (full suite 46), code-auditor reviewed (M1/M2/L1/L2/L3 fixed).

Design: docs/dedup-design-2026-06-24.md, docs/readiness-model-brief-2026-06-23.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kisa 2026-06-27 00:22:01 -04:00
parent e3de616023
commit c07d054ca5
4 changed files with 602 additions and 1 deletions

View file

@ -18,7 +18,7 @@ Canonical, git-tracked record of **what shipped, what didn't, and the reason or
| 6 | Convex-Signal spike **design** | Pi | **SHIPPED** | docs/convex-signal-spike-design-2026-06-24.md. 40h/3-phase plan; Module map + Repository interface + Convex schema + Auth seam (Convex default, Clerk swappable) + Phase 1 (core readiness loop) = what Claude builds first. | Claude builds Phase 1 from it |
| 7 | `signal-convex` repo creation | Claude (GitHub) + Kisa/token (Forgejo) | **PENDING** | Dependency: Pi design + Kisa approval (create once, correctly). Claude can do GitHub via `gh`; Forgejo has no API token here. | after design approved |
| 8 | Convex BAA email (plan/cost/signing) | Kisa | **IN PROGRESS** | Kisa handling | Kisa sends |
| 9 | Readiness-model build (rename coverage→readiness, rules-to-config, grouping+verdict, device override, require-plan-type, frontend nesting) | Claude | **UNBLOCKED** | Dedup decision locked: (patient_id, device_type, DOS). Order_number is display-only. Scales to all DMEPOS categories. Design: `docs/dedup-design-2026-06-24.md`. | Claude builds from dedup design + readiness model brief |
| 9 | Readiness-model build (rename coverage→readiness, rules-to-config, grouping+verdict, device override, require-plan-type, frontend nesting) | Claude | **IN PROGRESS** | Step 3 grouping half SHIPPED 2026-06-26: `core/dedup.py` (CSVRow→MergedShipment→CoverageLine) + 30 tests green, code-auditor reviewed (no High; M1/M2/L1/L2/L3 fixed). Added additive `plan_type` field to ShipmentRecord (default None; live grading untouched). NOT yet wired into the live pipeline. Remaining: required-vs-input verdict engine, rules-to-addressable-config (IDs), device-keyed override+recompute+rollup, require-plan-type enforcement + remove name-guesser/default, frontend nesting + "plan type needed" state. | Next: build the verdict engine + rules-to-config; WIRE orchestration in `api/main.py` or a new pipeline module, NOT inside `coverage_calculator` (dedup imports from it → circular import). Each step code-auditor-reviewed before ship. |
| 10 | NocoDB leads / CRM cleanup | Claude + Kisa | **BLOCKED** | Reason: leads table has no primary key (Signal-base data source `bdmhi02nks1pg6g` is corrupted — a fresh table there also came up PK-less; content base is healthy). SQLite can't add a PK to a populated table (Route 1 failed). Interim: capture still works (inserts via n8n `signal-cgm-lead`); the 2 test rows can't be deleted. | resolved by the data-platform decision (item 11) |
| 11 | Data-platform decision (leads + future PHI backend) | Kisa (with Claude/Pi analysis) | **OPEN** | Options analyzed: repaired-NocoDB / Supabase Postgres / Neon / AWS (already set up, BAA+RDS+S3 parked) / Convex ($25/mo incl. BAA). Leads = non-PHI (stay outside PHI boundary). Decision informed by the Convex spike. | run spike → decide |
| 12 | Research-versioning Layer-1 (`research-autopush.sh` + Stop-hook entry) | Claude | **NOT STARTED** | Dependency: Pi design exists (`docs/research-versioning-design-2026-06-24.md`). Lower priority than product. | next infra slot |

View file

@ -76,6 +76,10 @@ class ShipmentRecord:
csv_transfer_from: Optional[str] = None # prior supplier name if transfer
order_number: Optional[str] = None # pass-through — billing system order/claim number
hcpcs: Optional[str] = None # pass-through — HCPCS code from supplier CSV
# Client-mapped plan type — REQUIRED for grading, NEVER derived from the payer name.
# medicare | medicare_advantage | medicaid | commercial | None(=plan type needed).
# The `payer` field above is the raw display name only (locked 2026-06-23).
plan_type: Optional[str] = None
@dataclass

View file

@ -0,0 +1,250 @@
"""
dedup.py
Signal STTIL Solutions
Groups normalized shipment rows into deduped shipments and gradeable coverage
lines. This is the structural foundation of the readiness model: it turns a
flat list of CSV-derived rows into the patient -> device -> shipment hierarchy
the worklist needs, BEFORE any grading happens.
Design: docs/dedup-design-2026-06-24.md (Pi, locked by Kisa 2026-06-24)
docs/readiness-model-brief-2026-06-23.md sections 3, 5, 7
LOCKED DEDUP KEY: (patient_id, device_type, date_of_service)
order_number is a displayed attribute, NEVER part of the dedup key.
Monthly resupplies are separate dates of service, NOT duplicates.
PLAN TYPE: carried through verbatim from the client-mapped field. It is NEVER
derived from the payer name. None means "plan type needed" -> keyed as
'unknown' so the line still forms but cannot be graded green later.
PHI CONTRACT: operates only on patient_id, device_type, shipment_date,
quantity, payer (display), plan_type, component, order_number, hcpcs, and
the csv_* doc-status fields. No names, SSNs, DOBs, or contact fields.
"""
from __future__ import annotations
import hashlib
import logging
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import date, datetime
from typing import Optional
from core.coverage_calculator import ShipmentRecord, CoverageFlag
logger = logging.getLogger(__name__)
def _hash_pid(patient_id: str) -> str:
"""Short hashed patient_id for log lines — raw patient_id is never logged."""
return hashlib.sha256(patient_id.encode()).hexdigest()[:12]
def _as_date(value) -> date:
"""Coerce a datetime to a pure date so same-day timestamps group together."""
return value.date() if isinstance(value, datetime) else value
# CGM device types graded this phase. Pumps and other DMEPOS categories are
# preserved and displayed but not graded (gradeable=False). Mirrors the CGM
# devices in payer_rules.json; moves into addressable config in a later step.
CGM_DEVICE_TYPES = frozenset(
{"dexcom_g6", "dexcom_g7", "freestyle_libre_2", "freestyle_libre_3"}
)
UNKNOWN_PLAN_TYPE = "unknown"
def is_cgm_device(device_type: str) -> bool:
"""True only for CGM device types graded in the current phase."""
return device_type in CGM_DEVICE_TYPES
@dataclass
class MergedShipment:
"""One deduped shipment: all rows sharing (patient_id, device_type, DOS)."""
patient_id: str
device_type: str
shipment_date: date
total_quantity: int
components: list[str] # combined, deduped, first-seen order
order_numbers: list[str] # collected, deduped — display only
hcpcs_codes: list[str] # collected, deduped — display only
payer: str # raw display NAME, latest non-null
plan_type: Optional[str] # client-mapped; NEVER from payer name
# csv_* doc fields — latest non-null wins across the group
csv_visit_date: Optional[date] = None
csv_swo_status: Optional[str] = None
csv_pecos_verified: Optional[str] = None
csv_pa_status: Optional[str] = None
csv_diagnosis_on_file: Optional[str] = None
csv_transfer_from: Optional[str] = None
@dataclass(frozen=True)
class CoverageLineKey:
patient_id: str
device_type: str
plan_type_id: str
@dataclass
class CoverageLine:
"""A gradeable unit: one patient's one device under one plan type.
line_status and timing_flag are computed later by the grading step
(gradeLine); a freshly assigned line is intentionally ungraded.
"""
patient_id: str
device_type: str
plan_type: str
gradeable: bool
current_shipment: MergedShipment
prior_shipments: list[MergedShipment] = field(default_factory=list)
line_status: Optional[str] = None
timing_flag: Optional[CoverageFlag] = None
def _qty(record: ShipmentRecord) -> int:
"""A row without a quantity is still a shipment -> default to 1."""
return 1 if record.quantity is None else record.quantity
def _merge_quantities(rows: list[ShipmentRecord]) -> int:
"""Sum quantities across DISTINCT component configs, max within a config.
Within one component config, identical rows are likely data-entry
duplicates, so take the MAX (guards against doubling). Across distinct
configs (a real component split, e.g. sensor + transmitter), SUM.
A disagreement within one config (e.g. qty 3 and qty 100 for the same
component) collapses to the max and leaves a warning, so silently dropping
quantity always leaves an audit breadcrumb.
"""
by_config: dict[frozenset, list[int]] = defaultdict(list)
for row in rows:
by_config[frozenset({row.component})].append(_qty(row))
total = 0
for config, qtys in by_config.items():
if len(set(qtys)) > 1:
logger.warning(
"Quantity disagreement in dedup group patient=%s device=%s "
"component=%s qtys=%s -> taking max",
_hash_pid(rows[0].patient_id), rows[0].device_type,
set(config), qtys,
)
total += max(qtys)
return total
def _merge_plan_type(rows: list[ShipmentRecord]) -> Optional[str]:
"""Plan type is grading-critical and must NEVER be guessed.
If a dedup group carries more than one distinct non-null plan type, the data
is ambiguous, so return None ('plan type needed') rather than pick one.
"""
distinct = {r.plan_type for r in rows if r.plan_type}
if len(distinct) > 1:
logger.warning(
"Conflicting plan_type in dedup group patient=%s device=%s: %s "
"-> routing to 'plan type needed'",
_hash_pid(rows[0].patient_id), rows[0].device_type, sorted(distinct),
)
return None
return next(iter(distinct)) if distinct else None
def _dedup_preserve_order(values: list[Optional[str]]) -> list[str]:
"""Collect non-empty values, dedup, preserve first-seen order."""
seen: dict[str, None] = {}
for v in values:
if v:
seen.setdefault(v, None)
return list(seen.keys())
def _latest_non_null(rows: list[ShipmentRecord], attr: str):
"""Last meaningful value for an attribute, in ingestion order.
Empty / whitespace-only strings are treated as null so a blank later row
never clobbers an earlier real value (does not rely on upstream nulling).
"""
result = None
for row in rows:
value = getattr(row, attr)
if value is None:
continue
if isinstance(value, str) and value.strip() == "":
continue
result = value
return result
def dedup(records: list[ShipmentRecord]) -> list[MergedShipment]:
"""Group rows by (patient_id, device_type, shipment_date) into MergedShipments."""
groups: dict[tuple, list[ShipmentRecord]] = {}
for record in records:
key = (record.patient_id, record.device_type, _as_date(record.shipment_date))
groups.setdefault(key, []).append(record)
merged: list[MergedShipment] = []
for (patient_id, device_type, shipment_date), rows in groups.items():
merged.append(
MergedShipment(
patient_id=patient_id,
device_type=device_type,
shipment_date=shipment_date,
total_quantity=_merge_quantities(rows),
components=_dedup_preserve_order([r.component for r in rows]),
order_numbers=_dedup_preserve_order([r.order_number for r in rows]),
hcpcs_codes=_dedup_preserve_order([r.hcpcs for r in rows]),
payer=_latest_non_null(rows, "payer") or "",
plan_type=_merge_plan_type(rows),
csv_visit_date=_latest_non_null(rows, "csv_visit_date"),
csv_swo_status=_latest_non_null(rows, "csv_swo_status"),
csv_pecos_verified=_latest_non_null(rows, "csv_pecos_verified"),
csv_pa_status=_latest_non_null(rows, "csv_pa_status"),
csv_diagnosis_on_file=_latest_non_null(rows, "csv_diagnosis_on_file"),
csv_transfer_from=_latest_non_null(rows, "csv_transfer_from"),
)
)
merged.sort(key=lambda m: (m.patient_id, m.device_type, m.shipment_date))
return merged
def assign_to_coverage_lines(
merged_shipments: list[MergedShipment],
) -> dict[CoverageLineKey, CoverageLine]:
"""Group MergedShipments into CoverageLines by (patient_id, device_type, plan_type).
plan_type is the client-mapped field. None is keyed as 'unknown' (plan type
needed) so the line still forms but cannot be graded green later. The latest
DOS in each line is the current shipment; earlier ones are collapsed history.
"""
by_key: dict[CoverageLineKey, list[MergedShipment]] = {}
for shipment in merged_shipments:
key = CoverageLineKey(
patient_id=shipment.patient_id,
device_type=shipment.device_type,
plan_type_id=shipment.plan_type or UNKNOWN_PLAN_TYPE,
)
by_key.setdefault(key, []).append(shipment)
lines: dict[CoverageLineKey, CoverageLine] = {}
for key, shipments in by_key.items():
shipments.sort(key=lambda s: s.shipment_date, reverse=True)
lines[key] = CoverageLine(
patient_id=key.patient_id,
device_type=key.device_type,
plan_type=key.plan_type_id,
gradeable=is_cgm_device(key.device_type),
current_shipment=shipments[0],
prior_shipments=shipments[1:],
line_status=None,
timing_flag=None,
)
return lines

347
tests/test_dedup.py Normal file
View file

@ -0,0 +1,347 @@
"""
Tests for the dedup / grouping foundation of the readiness model.
Source design: docs/dedup-design-2026-06-24.md (Pi, locked by Kisa 2026-06-24)
docs/readiness-model-brief-2026-06-23.md sections 3, 5, 7
Locked dedup key: (patient_id, device_type, date_of_service).
order_number is a displayed attribute, NEVER part of the dedup key.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "python-backend"))
from datetime import date, timedelta
from core.coverage_calculator import ShipmentRecord
from core.dedup import (
dedup,
assign_to_coverage_lines,
MergedShipment,
CoverageLine,
is_cgm_device,
)
DOS = date(2026, 6, 1)
def make_row(**kwargs):
defaults = dict(
patient_id="P1",
device_type="dexcom_g7",
shipment_date=DOS,
quantity=3,
payer="Medicare Part B",
component="sensor",
plan_type="medicare",
)
defaults.update(kwargs)
return ShipmentRecord(**defaults)
# ---------------------------------------------------------------------------
# Dedup: grouping by (patient_id, device_type, DOS)
# ---------------------------------------------------------------------------
def test_dedup_component_split():
"""Two rows, same patient+device+DOS, different components -> one MergedShipment, summed qty."""
rows = [
make_row(quantity=3, component="sensor", order_number="ORD-100"),
make_row(quantity=1, component="transmitter", order_number="ORD-101"),
]
result = dedup(rows)
assert len(result) == 1
assert result[0].total_quantity == 4
assert set(result[0].components) == {"sensor", "transmitter"}
assert len(result[0].order_numbers) == 2
def test_dedup_identical_duplicate():
"""Two identical rows (same component) -> max quantity, not sum (duplicate guard)."""
rows = [
make_row(quantity=3, component="sensor"),
make_row(quantity=3, component="sensor"),
]
result = dedup(rows)
assert len(result) == 1
assert result[0].total_quantity == 3 # max within config, not 6
def test_dedup_mixed_duplicate_and_split():
"""Two duplicate sensor rows + one transmitter row -> max(sensor)+max(transmitter) = 4."""
rows = [
make_row(quantity=3, component="sensor"),
make_row(quantity=3, component="sensor"),
make_row(quantity=1, component="transmitter"),
]
result = dedup(rows)
assert result[0].total_quantity == 4
def test_dedup_zero_quantity_row_does_not_add():
"""qty=3 sensor + qty=0 sensor -> 3 (zero row is informational)."""
rows = [
make_row(quantity=3, component="sensor"),
make_row(quantity=0, component="sensor"),
]
result = dedup(rows)
assert result[0].total_quantity == 3
def test_dedup_wildly_different_same_component_takes_max():
"""qty=3 + qty=100 same component -> max 100 (likely error, not a real split)."""
rows = [
make_row(quantity=3, component="sensor"),
make_row(quantity=100, component="sensor"),
]
result = dedup(rows)
assert result[0].total_quantity == 100
def test_dedup_different_dos():
"""Same patient+device, different DOS -> two separate shipments."""
rows = [
make_row(shipment_date=DOS, quantity=3),
make_row(shipment_date=DOS + timedelta(days=30), quantity=3),
]
result = dedup(rows)
assert len(result) == 2
def test_dedup_different_device():
"""Same patient+DOS, different device -> two separate shipments."""
rows = [
make_row(device_type="dexcom_g7", component="sensor"),
make_row(device_type="omnipod_5", component="pod"),
]
result = dedup(rows)
assert len(result) == 2
def test_dedup_no_quantity_defaults_to_one():
"""Missing quantity -> default to 1 (a row without qty is still a shipment)."""
rows = [make_row(quantity=None, component="sensor")]
result = dedup(rows)
assert result[0].total_quantity == 1
def test_dedup_missing_order_number():
"""No order_number -> order_numbers list is empty, no crash."""
rows = [make_row(order_number=None)]
result = dedup(rows)
assert result[0].order_numbers == []
def test_dedup_order_numbers_collected_and_deduped():
"""Repeated order numbers across rows are collected once, in first-seen order."""
rows = [
make_row(component="sensor", order_number="ORD-100"),
make_row(component="transmitter", order_number="ORD-100"),
]
result = dedup(rows)
assert result[0].order_numbers == ["ORD-100"]
def test_dedup_hcpcs_collected():
"""HCPCS codes collected across rows in the group."""
rows = [
make_row(component="sensor", hcpcs="A4239"),
make_row(component="transmitter", hcpcs="E2103"),
]
result = dedup(rows)
assert set(result[0].hcpcs_codes) == {"A4239", "E2103"}
def test_dedup_payer_latest_non_null_wins():
"""payer is display-only; latest non-null row wins."""
rows = [
make_row(component="sensor", payer="Old Payer"),
make_row(component="transmitter", payer="Medicare Part B"),
]
result = dedup(rows)
assert result[0].payer == "Medicare Part B"
def test_dedup_csv_field_latest_non_null_wins():
"""csv_swo_status: a later non-null value wins over an earlier null."""
rows = [
make_row(component="sensor", csv_swo_status=None),
make_row(component="transmitter", csv_swo_status="On File"),
]
result = dedup(rows)
assert result[0].csv_swo_status == "On File"
def test_dedup_csv_field_earlier_non_null_kept_when_later_is_null():
"""csv_swo_status: an earlier non-null is kept when the later row is null."""
rows = [
make_row(component="sensor", csv_swo_status="On File"),
make_row(component="transmitter", csv_swo_status=None),
]
result = dedup(rows)
assert result[0].csv_swo_status == "On File"
def test_dedup_single_row_passthrough():
"""A single row produces a MergedShipment carrying its fields."""
rows = [make_row(quantity=2, component="sensor", order_number="ORD-1")]
result = dedup(rows)
assert len(result) == 1
assert isinstance(result[0], MergedShipment)
assert result[0].patient_id == "P1"
assert result[0].device_type == "dexcom_g7"
assert result[0].shipment_date == DOS
assert result[0].total_quantity == 2
assert result[0].plan_type == "medicare"
def test_dedup_plan_type_never_derived_from_payer():
"""plan_type carries the client-mapped value; None stays None even with a known payer name."""
rows = [make_row(payer="Aetna PPO", plan_type=None)]
result = dedup(rows)
assert result[0].plan_type is None # never guessed from "Aetna"
# ---------------------------------------------------------------------------
# CoverageLine assignment: group MergedShipments into gradeable lines
# ---------------------------------------------------------------------------
def test_is_cgm_device():
assert is_cgm_device("dexcom_g7") is True
assert is_cgm_device("freestyle_libre_3") is True
assert is_cgm_device("omnipod_5") is False
def test_assign_groups_by_patient_device_plan():
"""One patient, one device, one plan -> one CoverageLine."""
merged = dedup([
make_row(shipment_date=DOS, quantity=3),
make_row(shipment_date=DOS + timedelta(days=30), quantity=3),
])
lines = assign_to_coverage_lines(merged)
assert len(lines) == 1
def test_assign_latest_dos_is_current():
"""The latest DOS shipment is the current one; older ones are prior."""
merged = dedup([
make_row(shipment_date=DOS, quantity=3),
make_row(shipment_date=DOS + timedelta(days=30), quantity=3),
])
line = list(assign_to_coverage_lines(merged).values())[0]
assert line.current_shipment.shipment_date == DOS + timedelta(days=30)
assert len(line.prior_shipments) == 1
assert line.prior_shipments[0].shipment_date == DOS
def test_assign_cgm_line_is_gradeable():
merged = dedup([make_row(device_type="dexcom_g7", component="sensor")])
line = list(assign_to_coverage_lines(merged).values())[0]
assert line.gradeable is True
def test_assign_pump_line_not_gradeable():
merged = dedup([make_row(device_type="omnipod_5", component="pod")])
line = list(assign_to_coverage_lines(merged).values())[0]
assert line.gradeable is False
def test_assign_different_plan_type_separate_lines():
"""Same patient+device, different mapped plan_type -> two CoverageLines (dual-eligible)."""
merged = dedup([
make_row(shipment_date=DOS, plan_type="medicare"),
make_row(shipment_date=DOS + timedelta(days=30), plan_type="medicaid"),
])
lines = assign_to_coverage_lines(merged)
assert len(lines) == 2
def test_assign_none_plan_type_groups_as_unknown():
"""plan_type None is keyed as 'unknown', not guessed, and still forms a line."""
merged = dedup([make_row(plan_type=None)])
lines = assign_to_coverage_lines(merged)
assert len(lines) == 1
line = list(lines.values())[0]
assert line.plan_type == "unknown"
def test_assign_starts_ungraded():
"""A freshly assigned line has no status yet (gradeLine runs later)."""
merged = dedup([make_row()])
line = list(assign_to_coverage_lines(merged).values())[0]
assert line.line_status is None
assert line.timing_flag is None
# ---------------------------------------------------------------------------
# Hardening (code-auditor findings M1, L1, L3 + edge cases)
# ---------------------------------------------------------------------------
def test_dedup_conflicting_plan_type_routes_to_unknown():
"""Two rows in one group with DIFFERENT non-null plan types -> never guess.
The plan type is grading-critical and must never be silently picked. When a
dedup group disagrees, route to None so the line keys as 'plan type needed'.
"""
rows = [
make_row(component="sensor", plan_type="medicare"),
make_row(component="transmitter", plan_type="medicaid"),
]
result = dedup(rows)
assert len(result) == 1
assert result[0].plan_type is None
line = list(assign_to_coverage_lines(result).values())[0]
assert line.plan_type == "unknown"
def test_dedup_agreeing_plan_type_preserved():
"""Same non-null plan type across rows is kept (no false conflict)."""
rows = [
make_row(component="sensor", plan_type="medicare"),
make_row(component="transmitter", plan_type="medicare"),
]
result = dedup(rows)
assert result[0].plan_type == "medicare"
def test_dedup_empty_string_csv_field_does_not_clobber():
"""An empty-string csv value must not overwrite an earlier real value."""
rows = [
make_row(component="sensor", csv_swo_status="On File"),
make_row(component="transmitter", csv_swo_status=""),
]
result = dedup(rows)
assert result[0].csv_swo_status == "On File"
def test_dedup_datetime_dos_coerced_to_date():
"""Same calendar day with different timestamps -> one group (timezone edge)."""
from datetime import datetime
rows = [
make_row(shipment_date=datetime(2026, 6, 1, 9, 0), component="sensor"),
make_row(shipment_date=datetime(2026, 6, 1, 17, 0), component="transmitter"),
]
result = dedup(rows)
assert len(result) == 1
assert result[0].shipment_date == date(2026, 6, 1)
def test_dedup_empty_input():
assert dedup([]) == []
assert assign_to_coverage_lines([]) == {}
def test_cgm_device_types_exist_in_payer_rules():
"""Drift guard: every gradeable CGM device must exist in payer_rules.json,
and the known pump must not be in the gradeable set."""
import json
rules_path = (
Path(__file__).parent.parent
/ "python-backend" / "config" / "payer_rules.json"
)
devices = set(json.loads(rules_path.read_text())["devices"].keys())
from core.dedup import CGM_DEVICE_TYPES
missing = CGM_DEVICE_TYPES - devices
assert not missing, f"CGM devices missing from payer_rules.json: {missing}"
assert "omnipod_5" not in CGM_DEVICE_TYPES