diff --git a/src/pfs/stakes.py b/src/pfs/stakes.py index 4a9e5c5..7da43bb 100644 --- a/src/pfs/stakes.py +++ b/src/pfs/stakes.py @@ -322,20 +322,37 @@ def new_codes_in( """The family's new payment events in *year* (R84): a code payable in *year* (status A/R/T) that was not payable, or absent, in year-1 (judged only when year-1 is in *payment*), and every curated link with - ``since == year``. A link target is never judged by payability (R84a). Pass the full - payment history: a truncated window makes a code look absent in year-1.""" + ``since == year``. A link target is never judged by payability (R84a). A year-1 before the + first supplied year is unknown, and so is a code whose row count equals the + longest history (a ``valuation(years=N)`` window cut) while starting after + the family's first year: neither is flagged (R84b).""" payable = { (v.code, v.year) for v in payment if not v.proposed and v.status.strip().upper() in PAID_STATUSES } - years = {v.year for v in payment if not v.proposed} + by_code: dict[str, set[int]] = {} + for v in payment: + if not v.proposed: + by_code.setdefault(v.code, set()).add(v.year) + first = min((min(ys) for ys in by_code.values()), default=year) + longest = max((len(ys) for ys in by_code.values()), default=0) + + def truncated(code: str) -> bool: + # ``valuation(years=N)`` keeps the last N vintages per code, so a code + # with N rows that starts after the family's first year may have lost + # its earlier rows: its year-1 is unknown, never "absent" (R84b) + ys = by_code.get(code, set()) + return bool(ys) and min(ys) > first and len(ys) == longest + targets = {getattr(link, "target", None) or link.code for link in links} # R84a: payability counts only for the family's own member codes; a link # target is new only in its link ``since`` year (G2211 became payable for # office visits in 2024 but joins the home-visit family in 2026) found = { - c for c, y in payable if y == year and year - 1 in years and c not in targets + c + for c, y in payable + if y == year and year - 1 >= first and c not in targets and not truncated(c) } found -= {c for c, y in payable if y == year - 1} found |= { diff --git a/tests/pfs/test_stakes.py b/tests/pfs/test_stakes.py index 83ddad8..4d98b61 100644 --- a/tests/pfs/test_stakes.py +++ b/tests/pfs/test_stakes.py @@ -477,3 +477,20 @@ def test_link_target_flags_only_in_its_since_year_not_when_first_payable(): payment.append(val("G0318", 2024, 1.0)) (m,) = money_by_specialty(payment, psps, year=2024, links=links) assert m.new_codes == ("G0318",) + + +def test_truncated_window_flags_nothing_in_its_first_year(): + # valuation(years=7) shape: predecessors 2016-2022, current codes cut to + # 2021-2027 (their earlier rows fell out of the window), a real new code + payment = [val("99324", y, 50.0) for y in range(2016, 2023)] + payment += [val("99350", y, 100.0) for y in range(2021, 2028)] + payment += [val("G0318", y, 15.0) for y in range(2023, 2028)] + psps = [ps(2015, "99350", "Family Practice", 10.0)] + new = { + y: money_by_specialty(payment, psps, year=y)[0].new_codes + for y in range(2016, 2028) + } + assert new[2021] == () # 99350's year-1 is outside its window + assert new[2016] == () # before the first supplied year + assert new[2023] == ("G0318",) + assert [y for y, c in new.items() if c] == [2023]