'replaced with two new codes; CPT codes 37252 and 37253' keeps naming the successors, 'the reference to CPT code 76000 was intentionally deleted' deletes no code, and in 'bundling of the deleted code, CPT 50395, with CPT code 74485' only 50395 is deleted. deletion_readings(text, code) gives the deleted/cpt_panel readings of one text with the fr_events logic.
966 lines
38 KiB
Python
966 lines
38 KiB
Python
"""pfs.lineage — dated events from RVU diffs and FR paragraphs, cross-checked."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
|
|
import duckdb
|
|
import pytest
|
|
|
|
from pfs.codetables import ensure_tables
|
|
from pfs.lineage import (
|
|
_expand_ranges,
|
|
cpt_events,
|
|
deletion_readings,
|
|
fr_events,
|
|
fr_events_bucketed,
|
|
lineage,
|
|
lineage_all,
|
|
rvu_events,
|
|
rvu_year_span,
|
|
seen_codes,
|
|
status_counts,
|
|
)
|
|
|
|
RVU_COLS = "hcpcs VARCHAR, mod VARCHAR, description VARCHAR, status_code VARCHAR, non_fac_total DOUBLE, year INTEGER"
|
|
|
|
|
|
def _cpt_section(con, year, item_key):
|
|
con.execute(
|
|
"INSERT INTO pfs.cpt_section VALUES (?,?,?,?,?,?,?,?,?,?)",
|
|
[year, item_key, "sec1", 1, "T", [], "T", "", "", ""],
|
|
)
|
|
|
|
|
|
def _cpt_reference(con, year, item_key, code, years, text="text"):
|
|
con.execute(
|
|
"INSERT INTO pfs.cpt_reference VALUES (?,?,?,?,?,?)",
|
|
[year, item_key, code, "cpt-changes", years, text],
|
|
)
|
|
|
|
|
|
def _cpt_code(con, year, item_key, code):
|
|
con.execute(
|
|
"INSERT INTO pfs.cpt_code VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
[
|
|
year,
|
|
item_key,
|
|
code,
|
|
"sec1",
|
|
"I",
|
|
"d",
|
|
"s",
|
|
[],
|
|
"t",
|
|
"",
|
|
False,
|
|
False,
|
|
False,
|
|
False,
|
|
False,
|
|
False,
|
|
False,
|
|
False,
|
|
False,
|
|
],
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def con():
|
|
c = duckdb.connect(":memory:")
|
|
ensure_tables(c)
|
|
c.execute(f"CREATE TABLE pfs.rvu ({RVU_COLS})")
|
|
rows = [
|
|
# table spans 2015..2026 (filler code keeps the span honest)
|
|
*[("00000", None, "filler", "A", 1.0, y) for y in range(2015, 2027)],
|
|
# 99487: B in 2015-2016, A from 2017; description rename 2021; +25% revalue 2022
|
|
("99487", None, "Cmplx chron care w/o pt vsit", "B", 0.0, 2015),
|
|
("99487", None, "Cmplx chron care w/o pt vsit", "B", 0.0, 2016),
|
|
("99487", None, "Cmplx chron care w/o pt vsit", "A", 2.00, 2017),
|
|
("99487", None, "Cmplx chron care w/o pt vsit", "A", 2.02, 2018),
|
|
("99487", None, "Cmplx chron care w/o pt vsit", "A", 2.05, 2019),
|
|
("99487", None, "Cmplx chron care w/o pt vsit", "A", 2.10, 2020),
|
|
("99487", None, "Cplx chrnc care 1st 60 min", "A", 2.12, 2021),
|
|
("99487", None, "Cplx chrnc care 1st 60 min", "A", 2.65, 2022),
|
|
*[
|
|
("99487", None, "Cplx chrnc care 1st 60 min", "A", 2.65, y)
|
|
for y in range(2023, 2027)
|
|
],
|
|
("99487", "26", "modifier row ignored", "A", 99.0, 2022),
|
|
# G2058: 2020 only
|
|
("G2058", None, "Ccm add 20min", "A", 1.0, 2020),
|
|
# 99439: 2021 onward
|
|
*[
|
|
("99439", "", "Chrnc care mgmt svc ea addl", "A", 1.0, y)
|
|
for y in range(2021, 2027)
|
|
],
|
|
]
|
|
c.executemany("INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?)", rows)
|
|
yield c
|
|
c.close()
|
|
|
|
|
|
class TestSpan:
|
|
def test_span(self, con):
|
|
assert rvu_year_span(con) == (2015, 2026)
|
|
|
|
|
|
class TestRvuEvents:
|
|
def test_status_descriptor_and_revalue(self, con):
|
|
ev = rvu_events(con, "99487")
|
|
kinds = [(e.year, e.kind, e.note) for e in ev]
|
|
assert (2017, "status_change", "B→A") in kinds
|
|
assert (
|
|
2021,
|
|
"descriptor_change",
|
|
"Cmplx chron care w/o pt vsit → Cplx chrnc care 1st 60 min",
|
|
) in kinds
|
|
assert (2022, "revalued", "+25.0%") in kinds
|
|
assert not any(
|
|
k == "appeared" for _, k, _ in kinds
|
|
) # present from the first table year
|
|
assert all(
|
|
e.source == "rvu" and e.anchored is False and e.code == "99487" for e in ev
|
|
)
|
|
|
|
def test_appeared_and_disappeared(self, con):
|
|
ev = rvu_events(con, "G2058")
|
|
assert [(e.year, e.kind) for e in ev] == [
|
|
(2020, "appeared"),
|
|
(2021, "disappeared"),
|
|
]
|
|
|
|
def test_appeared_only(self, con):
|
|
ev = rvu_events(con, "99439")
|
|
assert [(e.year, e.kind) for e in ev] == [(2021, "appeared")]
|
|
|
|
def test_unknown_code(self, con):
|
|
assert rvu_events(con, "99999") == []
|
|
|
|
|
|
class _Store:
|
|
def __init__(self):
|
|
self.con = sqlite3.connect(":memory:")
|
|
self.con.row_factory = sqlite3.Row
|
|
self.con.executescript(
|
|
"CREATE TABLE items (key TEXT PRIMARY KEY, title TEXT, date_published TEXT);"
|
|
"CREATE TABLE fr_anchors (item_key TEXT, p_id INTEGER, page INTEGER, ordinal INTEGER, text TEXT);"
|
|
)
|
|
|
|
def _con(self):
|
|
return self.con
|
|
|
|
def close(self):
|
|
self.con.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def store():
|
|
s = _Store()
|
|
s.con.executemany(
|
|
"INSERT INTO items VALUES (?,?,?)",
|
|
[
|
|
("DE2VH9PD", "Medicare Program; CY 2015 PFS Final Rule", "2014-11-13"),
|
|
(
|
|
"YBM4IZUS",
|
|
"Medicare Program; CY 2021 Payment Policies Under the PFS",
|
|
"2020-12-28",
|
|
),
|
|
(
|
|
"XFGGRBDH",
|
|
"Medicare and Medicaid Programs; CY 2025 Payment Policies",
|
|
"2024-07-31",
|
|
),
|
|
],
|
|
)
|
|
paras = [
|
|
(
|
|
"DE2VH9PD",
|
|
1251,
|
|
67716,
|
|
"Accordingly, we will adopt CPT code 99490 for Medicare CCM services, effective January 1, 2015 instead of the G code.",
|
|
),
|
|
(
|
|
"YBM4IZUS",
|
|
686,
|
|
84547,
|
|
"We also are finalizing our proposal to allow HCPCS code G2058 (which we are finalizing in this rule as new CPT code 99439, see the codes in section II.H.) to be billed concurrently with TCM.",
|
|
),
|
|
(
|
|
"YBM4IZUS",
|
|
1578,
|
|
84639,
|
|
"At the January 2020 RUC meeting, specialty societies requested a temporary crosswalk through CY 2021 between the value established by CMS for HCPCS code G2058 and the value of new CPT code 99439 (with a descriptor identical to G2058).",
|
|
),
|
|
(
|
|
"XFGGRBDH",
|
|
489,
|
|
61652,
|
|
"The CPT Editorial Panel also deleted three codes (99441-99443) for reporting telephone E/M services. We note that CPT codes 99441, 99442, and 99443, each are assigned provisional status on the Medicare telehealth services list, and would return to bundled status when the telehealth flexibilities expire.",
|
|
),
|
|
(
|
|
"XFGGRBDH",
|
|
512,
|
|
61700,
|
|
"The CPT Editorial Panel also deleted three codes (90867-90869) for reporting transcranial magnetic stimulation treatment.",
|
|
),
|
|
]
|
|
s.con.executemany(
|
|
"INSERT INTO fr_anchors VALUES (?,?,?,?,?)",
|
|
[(k, p, pg, p, t) for k, p, pg, t in paras],
|
|
)
|
|
yield s
|
|
s.close()
|
|
|
|
|
|
class TestFrEvents:
|
|
def test_adopted_cpt(self, store):
|
|
ev = fr_events(store, "99490")
|
|
assert [(e.year, e.kind, e.item_key, e.p_id) for e in ev] == [
|
|
(2015, "adopted_cpt", "DE2VH9PD", 1251)
|
|
]
|
|
assert ev[0].anchored is True and ev[0].source == "fr" and ev[0].page == 67716
|
|
assert not any(e.kind == "replaces" for e in ev)
|
|
|
|
def test_replaced_by_and_crosswalk(self, store):
|
|
ev = fr_events(store, "G2058")
|
|
kinds = {e.kind for e in ev}
|
|
assert "created" not in kinds
|
|
assert "replaces" not in kinds
|
|
assert "replaced_by" in kinds and "crosswalk" in kinds
|
|
to_kinds = {(e.year, e.kind, e.to_codes) for e in ev}
|
|
assert (2021, "replaced_by", "99439") in to_kinds
|
|
|
|
def test_replaces_from_the_successor_side(self, store):
|
|
ev = fr_events(store, "99439")
|
|
kinds = {(e.year, e.kind) for e in ev}
|
|
assert (2021, "created") in kinds # ¶686 "new CPT code 99439"
|
|
rep = [e for e in ev if e.kind == "replaces"]
|
|
assert rep and rep[0].from_codes == "G2058"
|
|
|
|
def test_deleted_bundled_and_telehealth(self, store):
|
|
kinds = {e.kind for e in fr_events(store, "99441")}
|
|
assert {"deleted", "bundled", "telehealth_list"} <= kinds
|
|
|
|
def test_fr_citation_page_number_is_not_a_crosswalk_mention(self, store):
|
|
# M1: an FR page citation ("91 FR 99490") is not a real mention of
|
|
# 99490 — it must be stripped before pattern matching, or a
|
|
# "crosswalk ... 91 FR 99490" sentence reads as a crosswalk event.
|
|
store.con.execute(
|
|
"INSERT INTO fr_anchors VALUES (?,?,?,?,?)",
|
|
(
|
|
"DE2VH9PD",
|
|
1500,
|
|
67999,
|
|
1500,
|
|
"We note the crosswalk methodology discussed at 91 FR 99490 for related codes.",
|
|
),
|
|
)
|
|
ev = fr_events(store, "99490")
|
|
assert not any(e.kind == "crosswalk" for e in ev)
|
|
|
|
def test_deleted_via_range_prefilter_middle_member(self, store):
|
|
# 90868 is a middle member of a "90867-90869" range and appears
|
|
# nowhere else in the paragraph literally — the SQL prefilter must
|
|
# still surface the row for a range-block LIKE match.
|
|
ev = fr_events(store, "90868")
|
|
assert any(e.kind == "deleted" for e in ev)
|
|
|
|
|
|
P1672 = (
|
|
"More specifically, in its February 2021 meeting, effective beginning in 2023, the CPT Editorial Panel "
|
|
"deleted the nine CPT codes in the Domiciliary, Rest Home (for example, Boarding Home), or Custodial Care "
|
|
"Services code family (CPT codes 99324-99328, and 99334-99337), and one CPT code in the Home Services family "
|
|
"(CPT code 99343), to merge these services with the eight remaining home visit services."
|
|
)
|
|
|
|
|
|
def _add_para(
|
|
store, key, p_id, text, title="CY 2023 PFS Final Rule", date="2022-11-18"
|
|
):
|
|
store.con.execute("INSERT OR IGNORE INTO items VALUES (?,?,?)", (key, title, date))
|
|
store.con.execute(
|
|
"INSERT INTO fr_anchors VALUES (?,?,?,?,?)", (key, p_id, 1, p_id, text)
|
|
)
|
|
|
|
|
|
class TestNewKinds:
|
|
def test_family_wide_deletion_sentence(self, store):
|
|
_add_para(store, "PETM225D", 1672, P1672)
|
|
for code in (
|
|
"99324", "99325", "99326", "99327", "99328",
|
|
"99334", "99335", "99336", "99337", "99343",
|
|
): # fmt: skip
|
|
kinds = {(e.kind, e.item_key, e.p_id) for e in fr_events(store, code)}
|
|
assert ("deleted", "PETM225D", 1672) in kinds, code
|
|
assert ("cpt_panel", "PETM225D", 1672) in kinds, code
|
|
# codes outside the ranges get nothing from the paragraph
|
|
assert fr_events(store, "99329") == []
|
|
assert fr_events(store, "99338") == []
|
|
|
|
def test_successor_codes_are_not_deleted(self, store):
|
|
# public FR text (CY 2023 PFS, 87 FR 45883 / 69440): the deleted
|
|
# domiciliary family and the home visit family it was merged into
|
|
text = (
|
|
"That said, in February 2021, the AMA CPT Editorial Panel deleted the "
|
|
"family of domiciliary codes, CPT codes 99324 to 99340, and merged the "
|
|
"services described by those codes into the existing family of "
|
|
"home-based E/M visits, CPT codes 99341 to 99350 (a range of codes "
|
|
"that includes CPT codes 99344, 99345, 99349, and 99350)."
|
|
)
|
|
_add_para(store, "MZ24MX5S", 294, text)
|
|
for code in ("99324", "99330", "99340"):
|
|
kinds = {e.kind for e in fr_events(store, code)}
|
|
assert {"deleted", "cpt_panel"} <= kinds, code
|
|
for code in ("99341", "99342", "99347", "99348", "99350"):
|
|
assert fr_events(store, code) == [], code
|
|
assert fr_events_bucketed(store, [code])[code] == [], code
|
|
for code in ("99324", "99340"):
|
|
assert fr_events_bucketed(store, [code])[code] == fr_events(store, code)
|
|
|
|
def test_p1672_still_deletes_only_the_deleted_codes(self, store):
|
|
_add_para(store, "PETM225D", 1672, P1672)
|
|
_add_para(
|
|
store,
|
|
"PETM225D",
|
|
1673,
|
|
"The eight remaining home services CPT codes (99341, 99342, 99344, "
|
|
"99345, and 99347-99350) were revised to describe Home or Residence "
|
|
"Services.",
|
|
)
|
|
for code in ("99324", "99328", "99334", "99337", "99343"):
|
|
kinds = {e.kind for e in fr_events(store, code)}
|
|
assert {"deleted", "cpt_panel"} <= kinds, code
|
|
for n in (99341, 99342, 99344, 99345, 99347, 99348, 99349, 99350):
|
|
assert not any(
|
|
e.kind in ("deleted", "cpt_panel") for e in fr_events(store, str(n))
|
|
), n
|
|
|
|
def test_other_successor_markers_and_revising_panel_events(self, store):
|
|
_add_para(
|
|
store,
|
|
"PETM225D",
|
|
1680,
|
|
"Code 99401 was deleted and replaced by 99402. "
|
|
"Code 99410 was deleted and crosswalked to 99411. "
|
|
"Code 99420 was deleted, with the three remaining codes kept as 99421.",
|
|
)
|
|
assert {e.kind for e in fr_events(store, "99401")} >= {"deleted"}
|
|
assert "deleted" not in {e.kind for e in fr_events(store, "99402")}
|
|
assert "deleted" not in {e.kind for e in fr_events(store, "99411")}
|
|
assert "deleted" not in {e.kind for e in fr_events(store, "99421")}
|
|
_add_para(
|
|
store,
|
|
"PETM225D",
|
|
1681,
|
|
"The CPT Editorial Panel revised CPT code 99430 into a timed code "
|
|
"and replaced by 99431 the old one.",
|
|
)
|
|
assert "cpt_panel" in {e.kind for e in fr_events(store, "99430")}
|
|
|
|
def test_creation_replacement_and_split_events_keep_cpt_panel(self, store):
|
|
# public FR text, CY 2027 PFS final (JCWARTJD ¶328/¶329), CY 2013
|
|
# (KZ5GRTNZ ¶919), CY 2021 (2AZVKTCE ¶725)
|
|
_add_para(
|
|
store,
|
|
"JCWARTJD",
|
|
328,
|
|
"As discussed in section II.D., at the September 2025 CPT Editorial "
|
|
"Panel meeting, CPT code 92507 was replaced with 10 new codes (92654, "
|
|
"92655, 92656, 92657, 92658, 92659, 92660, 92661, 92662, and 92663).",
|
|
)
|
|
_add_para(
|
|
store,
|
|
"JCWARTJD",
|
|
329,
|
|
"As such, we are finalizing the deletion of CPT codes 92507, 0362T, and "
|
|
"0373T and the addition of CPT codes 97148, 97149, 92654, and 92655 to "
|
|
"the Medicare Telehealth Services List.",
|
|
)
|
|
_add_para(
|
|
store,
|
|
"KZ5GRTNZ",
|
|
919,
|
|
"Effective January 1, 2013, the CPT Editorial Panel deleted CPT code "
|
|
"95920 and replaced it with CPT codes 95940 for continuous monitoring "
|
|
"and 95941 for remote monitoring.",
|
|
)
|
|
_add_para(
|
|
store,
|
|
"2AZVKTCE",
|
|
725,
|
|
"In 2018, the CPT Editorial Panel created CPT code 94617 (Exercise "
|
|
"test), and CPT code 94618 (Pulmonary stress testing) from the now "
|
|
"deleted CPT code 94620 (Pulmonary stress testing; simple).",
|
|
)
|
|
_add_para(
|
|
store,
|
|
"NV3Q43B7",
|
|
1611,
|
|
"For CY 2013, the CPT Editorial Panel deleted CPT codes 32420, 32421, "
|
|
"and 32422 and replaced them with CPT codes 32554 and 32555.",
|
|
)
|
|
_add_para(
|
|
store,
|
|
"DE2VH9PD",
|
|
1143,
|
|
"For CY 2015, the CPT Editorial Panel replaced CPT code 76645 "
|
|
"(Ultrasound, breast) with two codes, CPT codes 76641 and 76642.",
|
|
)
|
|
for code in ("32554", "32555", "76641", "76642"):
|
|
kinds = {e.kind for e in fr_events(store, code)}
|
|
assert "cpt_panel" in kinds and "deleted" not in kinds, code
|
|
assert "deleted" in {e.kind for e in fr_events(store, "32420")}
|
|
for code in ("92654", "92663", "95940", "95941", "94617", "94618"):
|
|
kinds = {e.kind for e in fr_events(store, code)}
|
|
assert "cpt_panel" in kinds, code
|
|
assert "deleted" not in kinds, code
|
|
assert fr_events_bucketed(store, [code])[code] == fr_events(store, code)
|
|
assert "deleted" in {e.kind for e in fr_events(store, "92507")}
|
|
assert "deleted" in {e.kind for e in fr_events(store, "95920")}
|
|
assert "deleted" in {e.kind for e in fr_events(store, "94620")}
|
|
assert "deleted" not in {e.kind for e in fr_events(store, "97148")}
|
|
|
|
def test_new_code_and_idioms_do_not_hide_a_deletion(self, store):
|
|
# 73DU6WUG ¶929, 9V89NVZG ¶839 / ¶1020, 2KVJ2HKX ¶636 (public FR text)
|
|
_add_para(
|
|
store,
|
|
"73DU6WUG",
|
|
929,
|
|
"The deleted predecessor code for this service, CPT code 15732, did "
|
|
"not include a plate, and if it is typical for the new CPT code 15730, "
|
|
"we believe so.",
|
|
)
|
|
_add_para(
|
|
store,
|
|
"9V89NVZG",
|
|
839,
|
|
"The commenter disagreed with CMS's reference to CPT code 27370, which "
|
|
"is being deleted, as a basis for the RVU between the deleted code, "
|
|
"CPT code 27370 and the new code, CPT code 27369.",
|
|
)
|
|
_add_para(
|
|
store,
|
|
"9V89NVZG",
|
|
1020,
|
|
"Even after taking into consideration the bundling of the deleted "
|
|
"code, CPT 50395, with CPT code 74485, we believe the new code, CPT "
|
|
"50436, is not reflected.",
|
|
)
|
|
_add_para(
|
|
store,
|
|
"2KVJ2HKX",
|
|
636,
|
|
"The Panel approved the revision of CPT codes 77402 and 77407 to "
|
|
"establish a family of codes and bundle imaging into the three CPT "
|
|
"codes, and the deletion of CPT codes 77385, 77386 and 77014.",
|
|
)
|
|
_add_para(
|
|
store,
|
|
"JCWARTJD",
|
|
330,
|
|
"We will be replacing deleted CPT code 92507 with CPT codes 92654, "
|
|
"92655, and 92663.",
|
|
)
|
|
assert "deleted" in {e.kind for e in fr_events(store, "92507")}
|
|
for code in ("92654", "92655", "92663"):
|
|
assert "deleted" not in {e.kind for e in fr_events(store, code)}, code
|
|
assert fr_events_bucketed(store, [code])[code] == fr_events(store, code)
|
|
for code in ("15732", "27370", "50395", "77385", "77386", "77014"):
|
|
assert "deleted" in {e.kind for e in fr_events(store, code)}, code
|
|
assert fr_events_bucketed(store, [code])[code] == fr_events(store, code)
|
|
for code in ("15730", "27369", "50436", "77402"):
|
|
assert "deleted" not in {e.kind for e in fr_events(store, code)}, code
|
|
|
|
def test_n7_successors_references_and_bundling_are_not_deleted(self, store):
|
|
# public FR text: CY 2017 PFS proposed (P8SNUR46 ¶971) and final
|
|
# (WHNGCYHB ¶1967), CY 2023 final (PETM225D ¶781), CY 2019 final
|
|
# (9V89NVZG ¶1020)
|
|
replaced = (
|
|
"Also we note that the intravascular codes for which we initially "
|
|
"established our base/add-on code relationship for CPT codes 88346 "
|
|
"and 88350 were deleted in CY 2016 and replaced with two new codes; "
|
|
"CPT codes 37252 and 37253. The relationship between 37252 and 37253 "
|
|
"represents a 20 percent discount for the add-on code."
|
|
)
|
|
_add_para(store, "P8SNUR46", 971, replaced, "CY 2017 PFS Proposed Rule")
|
|
_add_para(store, "WHNGCYHB", 1967, replaced, "CY 2017 PFS Final Rule")
|
|
_add_para(
|
|
store,
|
|
"PETM225D",
|
|
781,
|
|
"From our re-review of the AMA RUC recommendations, we do note that in "
|
|
"the text material accompanying the RUC recommendation for CPT code "
|
|
"76000 ( Fluoroscopy (separate procedure), up to 1 hour physician or "
|
|
"other qualified health care professional time) codes or language was "
|
|
"struck from the text material. It is unclear if the reference to CPT "
|
|
"code 76000 was intentionally deleted, but we note that CPT code 76000 "
|
|
"has a work RVU value of 0.30 and an intra-service time of 10.0 minutes.",
|
|
)
|
|
_add_para(
|
|
store,
|
|
"9V89NVZG",
|
|
1020,
|
|
"Response: Even after taking into consideration the bundling of the "
|
|
"deleted code, CPT 50395, with CPT code 74485, we believe that there are "
|
|
"efficiencies in the work that are not adequately reflected in the "
|
|
"RUC-recommended work RVU for this new code, CPT 50436.",
|
|
)
|
|
for code in ("37252", "37253", "76000", "74485", "50436"):
|
|
assert "deleted" not in {e.kind for e in fr_events(store, code)}, code
|
|
assert fr_events_bucketed(store, [code])[code] == fr_events(store, code)
|
|
ev = fr_events(store, "50395")
|
|
assert ("deleted", "9V89NVZG", 1020) in {
|
|
(e.kind, e.item_key, e.p_id) for e in ev
|
|
}
|
|
assert fr_events_bucketed(store, ["50395"])["50395"] == ev
|
|
# a ";" with a verb after it still ends the successor clause, and
|
|
# "bundled with" names what a deleted code went into
|
|
_add_para(
|
|
store,
|
|
"PETM225D",
|
|
1691,
|
|
"Code 99601 was replaced with 99602; 99603 was also deleted. "
|
|
"Code 99604 was deleted and bundled with 99605. "
|
|
"The reference to CPT code 99606, which is being deleted, stays.",
|
|
)
|
|
for code in ("99601", "99603", "99604", "99606"):
|
|
assert "deleted" in {e.kind for e in fr_events(store, code)}, code
|
|
for code in ("99602", "99605"):
|
|
assert "deleted" not in {e.kind for e in fr_events(store, code)}, code
|
|
|
|
def test_deletion_readings_of_one_text(self):
|
|
for code in ("99324", "99337", "99343"):
|
|
assert deletion_readings(P1672, code) == {"deleted", "cpt_panel"}, code
|
|
assert deletion_readings(P1672, "99341") == set()
|
|
merged = (
|
|
"The AMA CPT Editorial Panel deleted domiciliary codes 99324-99340 and "
|
|
"merged them into the existing home-based E/M visit codes 99341-99350."
|
|
)
|
|
assert deletion_readings(merged, "99330") == {"deleted", "cpt_panel"}
|
|
assert deletion_readings(merged, "99343") == set()
|
|
assert deletion_readings("Code 99343 was deleted.", "99343") == {"deleted"}
|
|
assert deletion_readings("Nothing about it.", "99343") == set()
|
|
|
|
def test_a_deletion_after_a_semicolon_is_not_masked(self, store):
|
|
_add_para(
|
|
store,
|
|
"PETM225D",
|
|
1690,
|
|
"Code 99501 was replaced by 99502; the Panel also deleted 99503.",
|
|
)
|
|
kinds = {e.kind for e in fr_events(store, "99503")}
|
|
assert "deleted" in kinds
|
|
|
|
def test_fr_page_lists_are_not_code_ranges(self, store):
|
|
assert _expand_ranges("see 79 FR 72760, 72790 through 72792 for") == (
|
|
"see 79 FR 72760, 72790 through 72792 for"
|
|
)
|
|
assert _expand_ranges("FR 72760 and 72790-72792, then 99341-99343") == (
|
|
"FR 72760 and 72790-72792, then 99341 99342 99343"
|
|
)
|
|
|
|
def test_en_dash_range(self, store):
|
|
_add_para(
|
|
store,
|
|
"PETM225D",
|
|
1673,
|
|
"The CPT Editorial Panel deleted codes 99324\u201399326 this year.",
|
|
)
|
|
ev = fr_events(store, "99325")
|
|
assert {e.kind for e in ev} == {"deleted", "cpt_panel"}
|
|
|
|
def test_prose_range_forms_through_and_to(self, store):
|
|
_add_para(
|
|
store,
|
|
"PETM225D",
|
|
1675,
|
|
"The CPT Editorial Panel deleted codes 99324 through 99326 this year, "
|
|
"and codes G0300 to G0302 too.",
|
|
)
|
|
_add_para(
|
|
store,
|
|
"PETM225D",
|
|
1676,
|
|
"The CPT Editorial Panel deleted codes 99410 to 99412 this year.",
|
|
)
|
|
for code in ("99325", "99411", "G0301"):
|
|
ev = fr_events(store, code)
|
|
assert {e.kind for e in ev} == {"deleted", "cpt_panel"}, code
|
|
assert fr_events_bucketed(store, [code])[code] == ev, code
|
|
assert fr_events(store, "99327") == []
|
|
assert _expand_ranges("99341 through 99343") == "99341 99342 99343"
|
|
assert _expand_ranges("99324 to 99326") == "99324 99325 99326"
|
|
assert _expand_ranges("G0300 to G0302") == "G0300 G0301 G0302"
|
|
# not a code range: values, and spans over the 50-code limit
|
|
assert (
|
|
_expand_ranges("increase from 1.56 to 2.00") == "increase from 1.56 to 2.00"
|
|
)
|
|
assert _expand_ranges("from 99100 to 99300") == "from 99100 to 99300"
|
|
# an FR page span is not a code range
|
|
for cite in ("79 FR 67912 through 67914", "79 FR 67912-67914"):
|
|
assert _expand_ranges(cite) == cite
|
|
|
|
def test_oversized_range_not_expanded(self, store):
|
|
_add_para(
|
|
store,
|
|
"PETM225D",
|
|
1674,
|
|
"The CPT Editorial Panel deleted codes 99100-99300 this year.",
|
|
)
|
|
assert fr_events(store, "99200") == []
|
|
|
|
def test_misvalued_event(self, store):
|
|
_add_para(
|
|
store,
|
|
"PETM225D",
|
|
1700,
|
|
"CMS identified CPT code 99350 as potentially misvalued in this rule.",
|
|
)
|
|
ev = fr_events(store, "99350")
|
|
assert [(e.kind, e.item_key, e.p_id) for e in ev] == [
|
|
("misvalued", "PETM225D", 1700)
|
|
]
|
|
_add_para(
|
|
store,
|
|
"PETM225D",
|
|
1701,
|
|
"Potentially misvalued codes this year include 99351.",
|
|
)
|
|
assert [e.kind for e in fr_events(store, "99351")] == ["misvalued"]
|
|
|
|
def test_bucketed_equals_per_code_for_new_kinds(self, store):
|
|
_add_para(store, "PETM225D", 1672, P1672)
|
|
_add_para(store, "PETM225D", 1700, "CPT code 99350 is potentially misvalued.")
|
|
for c in ("99324", "99337", "99343", "99350", "99339"):
|
|
assert fr_events_bucketed(store, [c])[c] == fr_events(store, c)
|
|
assert fr_events_bucketed(store, ["99339"])["99339"] == []
|
|
|
|
|
|
class TestSeenCodes:
|
|
def _con(self):
|
|
c = duckdb.connect(":memory:")
|
|
ensure_tables(c)
|
|
c.execute(f"CREATE TABLE pfs.rvu ({RVU_COLS})")
|
|
c.executemany(
|
|
"INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?)",
|
|
[
|
|
("99343", None, "d", "A", 1.0, 2022),
|
|
("99341", None, "d", "A", 1.0, 2022),
|
|
("99341", None, "d", "A", 1.0, 2027),
|
|
("\x1a", None, "junk", "A", 1.0, 2022),
|
|
("0001u", None, "cat3", "A", 1.0, 2022),
|
|
],
|
|
)
|
|
return c
|
|
|
|
def test_includes_deleted_and_curated_not_junk(self):
|
|
c = self._con()
|
|
_cpt_section(c, 2019, "ITEM2019")
|
|
_cpt_code(c, 2019, "ITEM2019", "99999")
|
|
codes = seen_codes(c)
|
|
assert {"99341", "99343", "99999", "0001U", "99324", "99340"} <= set(codes)
|
|
assert "\x1a" not in codes
|
|
assert codes == sorted(codes)
|
|
|
|
def test_missing_cpt_tables_tolerated(self):
|
|
c = duckdb.connect(":memory:")
|
|
c.execute("CREATE SCHEMA pfs")
|
|
c.execute(f"CREATE TABLE pfs.rvu ({RVU_COLS})")
|
|
c.execute("INSERT INTO pfs.rvu VALUES ('99343',NULL,'d','A',1.0,2022)")
|
|
assert "99343" in seen_codes(c)
|
|
|
|
def test_other_errors_propagate(self):
|
|
class Bad:
|
|
def execute(self, sql, *a):
|
|
if "cpt_code" in sql:
|
|
raise RuntimeError("boom")
|
|
return duckdb.connect(":memory:").execute("SELECT 'x' WHERE false")
|
|
|
|
with pytest.raises(RuntimeError):
|
|
seen_codes(Bad())
|
|
|
|
|
|
class TestDeletedCodeDisappears:
|
|
def test_rvu_disappeared_for_deleted_code(self):
|
|
c = duckdb.connect(":memory:")
|
|
ensure_tables(c)
|
|
c.execute(f"CREATE TABLE pfs.rvu ({RVU_COLS})")
|
|
rows = [
|
|
(f"0000{i}", None, "f", "A", 1.0, y)
|
|
for y in range(2015, 2028)
|
|
for i in range(9)
|
|
]
|
|
rows += [("99343", None, "d", "A", 1.0, y) for y in range(2015, 2023)]
|
|
c.executemany("INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?)", rows)
|
|
assert [(e.year, e.kind) for e in rvu_events(c, "99343")] == [
|
|
(2023, "disappeared")
|
|
]
|
|
|
|
|
|
class TestPartialYearDisappearance:
|
|
@staticmethod
|
|
def _con(m_last_year_kept):
|
|
c = duckdb.connect(":memory:")
|
|
ensure_tables(c)
|
|
c.execute(f"CREATE TABLE pfs.rvu ({RVU_COLS})")
|
|
rows = []
|
|
for y in (2025, 2026):
|
|
rows += [(f"A{i:04d}", None, "d", "A", 1.0, y) for i in range(10)]
|
|
rows += [(f"M{i:04d}", None, "d", "M", 1.0, y) for i in range(10)]
|
|
# final, partial year: 9 of 10 A codes (exactly 90%), M kept per arg
|
|
rows += [(f"A{i:04d}", None, "d", "A", 1.0, 2027) for i in range(9)]
|
|
rows += [
|
|
(f"M{i:04d}", None, "d", "M", 1.0, 2027) for i in range(m_last_year_kept)
|
|
]
|
|
c.executemany("INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?)", rows)
|
|
return c
|
|
|
|
def test_incomplete_status_gets_no_disappeared_and_complete_does(self):
|
|
c = self._con(0)
|
|
assert rvu_events(c, "M0001") == []
|
|
# A is at exactly 90% -> complete -> the dropped A code disappears
|
|
assert [(e.year, e.kind) for e in rvu_events(c, "A0009")] == [
|
|
(2027, "disappeared")
|
|
]
|
|
|
|
def test_just_below_ninety_percent_is_incomplete(self):
|
|
c = self._con(0)
|
|
c.execute("DELETE FROM pfs.rvu WHERE year = 2027 AND hcpcs = 'A0008'")
|
|
assert status_counts(c)[(2027, "A")] == 8
|
|
assert rvu_events(c, "A0009") == []
|
|
|
|
def test_status_at_ninety_percent_of_m_fires(self):
|
|
c = self._con(9)
|
|
assert [(e.year, e.kind) for e in rvu_events(c, "M0009")] == [
|
|
(2027, "disappeared")
|
|
]
|
|
|
|
|
|
class TestRetiredAndDipStatuses:
|
|
@staticmethod
|
|
def _con(rows):
|
|
c = duckdb.connect(":memory:")
|
|
ensure_tables(c)
|
|
c.execute(f"CREATE TABLE pfs.rvu ({RVU_COLS})")
|
|
c.executemany("INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?)", rows)
|
|
return c
|
|
|
|
def test_retired_status_fires_in_the_year_after_last_seen(self):
|
|
rows = [
|
|
(f"A{i:04d}", None, "d", "A", 1.0, y)
|
|
for y in (1, 2, 3, 4)
|
|
for i in range(10)
|
|
]
|
|
rows += [
|
|
(f"Q{i:04d}", None, "d", "Q", 1.0, y) for y in (1, 2) for i in range(5)
|
|
]
|
|
c = self._con(rows)
|
|
assert [(e.year, e.kind) for e in rvu_events(c, "Q0001")] == [
|
|
(3, "disappeared")
|
|
]
|
|
|
|
def test_one_year_dip_that_recovers_still_fires_in_the_dip_year(self):
|
|
rows = [
|
|
(f"A{i:04d}", None, "d", "A", 1.0, y) for y in (1, 2, 3) for i in range(10)
|
|
]
|
|
rows += [(f"M{i:04d}", None, "d", "M", 1.0, 1) for i in range(10)]
|
|
rows += [("M0000", None, "d", "M", 1.0, 2)]
|
|
rows += [(f"N{i:04d}", None, "d", "M", 1.0, 3) for i in range(10)]
|
|
c = self._con(rows)
|
|
# M0001 last seen in year 1, never again; year 2 dipped, year 3 recovered
|
|
assert [(e.year, e.kind) for e in rvu_events(c, "M0001")] == [
|
|
(2, "disappeared")
|
|
]
|
|
|
|
def test_partial_newest_year_with_no_later_file_is_suppressed(self):
|
|
rows = [
|
|
(f"A{i:04d}", None, "d", "A", 1.0, y) for y in (1, 2, 3) for i in range(10)
|
|
]
|
|
rows += [
|
|
(f"M{i:04d}", None, "d", "M", 1.0, y) for y in (1, 2) for i in range(10)
|
|
]
|
|
c = self._con(rows)
|
|
assert rvu_events(c, "M0001") == []
|
|
|
|
|
|
class TestExpandRangeBoundary:
|
|
def test_span_fifty_expands_fifty_one_does_not(self):
|
|
assert len(_expand_ranges("99100-99150").split()) == 51
|
|
assert _expand_ranges("99100-99151") == "99100-99151"
|
|
assert len(_expand_ranges("G0100-G0150").split()) == 51
|
|
assert _expand_ranges("G0100-G0151") == "G0100-G0151"
|
|
|
|
|
|
class TestExpandRanges:
|
|
def test_letter_range(self):
|
|
assert _expand_ranges("G0008-G0010") == "G0008 G0009 G0010"
|
|
|
|
|
|
class TestCptEvents:
|
|
def test_cpt_changed_one_per_year_from_newest_edition(self, con):
|
|
_cpt_section(con, 2019, "ITEM2019")
|
|
_cpt_section(con, 2024, "GQGTPGYV")
|
|
_cpt_reference(con, 2019, "ITEM2019", "99490", [2015])
|
|
_cpt_reference(con, 2024, "GQGTPGYV", "99490", [2015, 2021, 2022])
|
|
ev = cpt_events(con, "99490")
|
|
assert [(e.year, e.kind, e.item_key, e.note) for e in ev] == [
|
|
(2015, "cpt_changed", "GQGTPGYV", "CPT Changes 2015"),
|
|
(2021, "cpt_changed", "GQGTPGYV", "CPT Changes 2021"),
|
|
(2022, "cpt_changed", "GQGTPGYV", "CPT Changes 2022"),
|
|
]
|
|
assert all(e.source == "cpt" and e.anchored is True for e in ev)
|
|
assert all(e.code == "99490" for e in ev)
|
|
|
|
def test_cpt_deleted_when_absent_from_newest_edition(self, con):
|
|
_cpt_section(con, 2019, "ITEM2019")
|
|
_cpt_section(con, 2024, "GQGTPGYV")
|
|
_cpt_code(con, 2019, "ITEM2019", "99999")
|
|
ev = cpt_events(con, "99999")
|
|
assert [(e.year, e.kind, e.item_key, e.note) for e in ev] == [
|
|
(2020, "cpt_deleted", "ITEM2019", "absent from CPT 2024")
|
|
]
|
|
|
|
def test_no_events_for_a_code_absent_everywhere(self, con):
|
|
_cpt_section(con, 2024, "GQGTPGYV")
|
|
assert cpt_events(con, "00000") == []
|
|
|
|
def test_no_cpt_tables_yet_is_empty(self, con):
|
|
assert cpt_events(con, "99490") == []
|
|
|
|
def test_no_cpt_tables_at_all_does_not_raise(self):
|
|
# #685/#686 review finding: a replica that has never had
|
|
# cpt-ingest run has no pfs.cpt_* schema at all (not merely empty
|
|
# tables) — pfs.cpt_section itself doesn't exist, so cpt_years()
|
|
# raises CatalogException. cpt_events must swallow that (I4), the
|
|
# same way the CLI's read-only paths already tolerate a missing
|
|
# pfs.code_element/pfs.code_event.
|
|
c = duckdb.connect(":memory:")
|
|
c.execute("CREATE SCHEMA pfs")
|
|
c.execute(f"CREATE TABLE pfs.rvu ({RVU_COLS})")
|
|
try:
|
|
assert cpt_events(c, "99490") == []
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
class TestCrossCheck:
|
|
def test_lineage_with_no_cpt_tables_at_all_does_not_raise(self, store):
|
|
c = duckdb.connect(":memory:")
|
|
c.execute("CREATE SCHEMA pfs")
|
|
c.execute(f"CREATE TABLE pfs.rvu ({RVU_COLS})")
|
|
c.executemany(
|
|
"INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?)",
|
|
[
|
|
("00000", None, "filler", "A", 1.0, 2019),
|
|
("00000", None, "filler", "A", 1.0, 2021),
|
|
("G2058", None, "Ccm add 20min", "A", 1.0, 2020),
|
|
],
|
|
)
|
|
try:
|
|
ev = lineage(c, store, "G2058")
|
|
finally:
|
|
c.close()
|
|
assert not any(e.source == "cpt" for e in ev)
|
|
assert any(e.kind == "appeared" for e in ev)
|
|
|
|
def test_rvu_event_anchored_by_cpt_changed_within_one_year(self, con, store):
|
|
# 99487 has no FR paragraph in `store`, but a cpt_changed at 2016
|
|
# (Y-1) must still anchor the 2017 status_change.
|
|
_cpt_section(con, 2024, "GQGTPGYV")
|
|
_cpt_reference(con, 2024, "GQGTPGYV", "99487", [2016])
|
|
ev = lineage(con, store, "99487")
|
|
by = {(e.kind, e.year): e for e in ev}
|
|
assert by[("status_change", 2017)].anchored is True
|
|
assert ("cpt_changed", 2016) in by
|
|
|
|
def test_rvu_events_anchored_by_nearby_fr_event(self, con, store):
|
|
ev = lineage(con, store, "G2058")
|
|
by_kind = {e.kind: e for e in ev}
|
|
assert (
|
|
by_kind["appeared"].year == 2020 and by_kind["appeared"].anchored is True
|
|
) # 2021 is within ±1
|
|
assert (
|
|
by_kind["disappeared"].year == 2021
|
|
and by_kind["disappeared"].anchored is True
|
|
) # 2021 replaced_by
|
|
assert "replaced_by" in by_kind
|
|
assert all(
|
|
not e.anchored for e in lineage(con, store, "99487") if e.source == "rvu"
|
|
)
|
|
|
|
def test_sorted_by_year_then_source(self, con, store):
|
|
ev = lineage(con, store, "99439")
|
|
assert [e.year for e in ev] == sorted(e.year for e in ev)
|
|
|
|
|
|
class TestFrEventsBucketed:
|
|
# The four target codes from `store`'s three rule items: a plain
|
|
# mention (99490), a replaced_by/crosswalk pair (G2058 -> 99439) and
|
|
# a range-form deletion (99441, from "99441-99443").
|
|
_CODES = ("99490", "G2058", "99439", "99441")
|
|
|
|
def test_matches_fr_events_per_code(self, store):
|
|
bucketed = fr_events_bucketed(store, self._CODES)
|
|
for c in self._CODES:
|
|
assert bucketed[c] == fr_events(store, c), c
|
|
|
|
def test_range_form_middle_member_matches(self, store):
|
|
# 90868 is a middle member of a "90867-90869" range and appears
|
|
# nowhere else in the paragraph literally — both paths must find
|
|
# it via range expansion, not a literal LIKE hit.
|
|
bucketed = fr_events_bucketed(store, ["90868"])
|
|
assert bucketed["90868"] == fr_events(store, "90868")
|
|
assert any(e.kind == "deleted" for e in bucketed["90868"])
|
|
|
|
def test_fr_citation_page_number_is_not_a_mention(self, store):
|
|
# M1, bucketed path: a page citation ("91 FR 99490") must not
|
|
# make 99490 register a hit at all, not just skip the crosswalk
|
|
# pattern — matching fr_events's own guard.
|
|
store.con.execute(
|
|
"INSERT INTO fr_anchors VALUES (?,?,?,?,?)",
|
|
(
|
|
"DE2VH9PD",
|
|
1500,
|
|
67999,
|
|
1500,
|
|
"We note the crosswalk methodology discussed at 91 FR 99490 for related codes.",
|
|
),
|
|
)
|
|
bucketed = fr_events_bucketed(store, ["99490"])
|
|
assert bucketed["99490"] == fr_events(store, "99490")
|
|
assert not any(e.kind == "crosswalk" for e in bucketed["99490"])
|
|
|
|
def test_unmentioned_code_gets_empty_bucket(self, store):
|
|
assert fr_events_bucketed(store, ["00000"]) == {"00000": []}
|
|
|
|
|
|
class TestLineageAll:
|
|
def test_matches_lineage_per_code(self, con, store):
|
|
codes = ["99487", "G2058", "99439"]
|
|
by_code = lineage_all(con, store, codes)
|
|
for c in codes:
|
|
assert by_code[c] == lineage(con, store, c), c
|
|
|
|
def test_rvu_event_anchored_by_cpt_changed_within_one_year(self, con, store):
|
|
_cpt_section(con, 2024, "GQGTPGYV")
|
|
_cpt_reference(con, 2024, "GQGTPGYV", "99487", [2016])
|
|
by_code = lineage_all(con, store, ["99487"])
|
|
by = {(e.kind, e.year): e for e in by_code["99487"]}
|
|
assert by[("status_change", 2017)].anchored is True
|
|
assert ("cpt_changed", 2016) in by
|
|
|
|
def test_rvu_events_anchored_by_nearby_fr_event(self, con, store):
|
|
by_code = lineage_all(con, store, ["G2058", "99487"])
|
|
by_kind = {e.kind: e for e in by_code["G2058"]}
|
|
assert by_kind["appeared"].year == 2020 and by_kind["appeared"].anchored is True
|
|
assert (
|
|
by_kind["disappeared"].year == 2021
|
|
and by_kind["disappeared"].anchored is True
|
|
)
|
|
assert all(not e.anchored for e in by_code["99487"] if e.source == "rvu")
|
|
|
|
def test_unknown_code_yields_empty_list(self, con, store):
|
|
assert lineage_all(con, store, ["99999"]) == {"99999": []}
|