diff --git a/src/pipelines/shared/extraction/tin_npi_funcs.py b/src/pipelines/shared/extraction/tin_npi_funcs.py index 6f96b15..d1731a0 100644 --- a/src/pipelines/shared/extraction/tin_npi_funcs.py +++ b/src/pipelines/shared/extraction/tin_npi_funcs.py @@ -344,6 +344,36 @@ def add_group_and_other(one_to_one_results: dict[str, Any], filename: str): provider_name = one_to_one_results.get("PROVIDER_NAME") # Can be List or string prov_info_json = one_to_one_results["PROV_INFO_JSON"] + # Fallback when no TIN/NPI identifiers are present in the contract text: + # the regex gate in get_prov_info_json short-circuits the LLM call, leaving + # PROV_INFO_JSON empty even when PROVIDER_NAME was successfully extracted. + # Synthesize NAME-only entries so the field consistently represents the + # provider. By construction these entries are the group provider, so set + # IS_GROUP and the group_*/other_* fields directly and return early — this + # avoids an unnecessary provider_name_match_check LLM call. + if not prov_info_json and not string_utils.is_empty(provider_name): + names = provider_name if isinstance(provider_name, list) else [provider_name] + valid_names = [str(n).strip() for n in names if not string_utils.is_empty(n)] + if valid_names: + for name in valid_names: + prov_info_json.append( + { + "TIN": "N/A", + "NPI": "N/A", + "NAME": name, + "FROM_FILENAME": "N", + "IS_GROUP": "Y", + } + ) + one_to_one_results["PROV_INFO_JSON"] = prov_info_json + one_to_one_results["PROV_GROUP_TIN"] = [] + one_to_one_results["PROV_GROUP_NPI"] = [] + one_to_one_results["PROV_GROUP_NAME_FULL"] = valid_names + one_to_one_results["PROV_OTHER_TIN"] = [] + one_to_one_results["PROV_OTHER_NPI"] = [] + one_to_one_results["PROV_OTHER_NAME_FULL"] = [] + return one_to_one_results + group_tins, group_npis, group_names = set(), set(), set() other_tins, other_npis, other_names = set(), set(), set() diff --git a/src/tests/test_tin_npi_funcs.py b/src/tests/test_tin_npi_funcs.py index 6e1bb51..1c2665c 100644 --- a/src/tests/test_tin_npi_funcs.py +++ b/src/tests/test_tin_npi_funcs.py @@ -336,3 +336,104 @@ class TestTinNpiFuncs: assert len(result["PROV_INFO_JSON"]) == 3 # Sort both lists for comparison since order may not be deterministic assert sorted(result["FILENAME_TIN"]) == sorted(["123456789", "987654321"]) + + def test_add_group_fallback_when_empty_and_name_str(self): + """Empty PROV_INFO_JSON with a string PROVIDER_NAME synthesizes one entry.""" + one_to_one_results = { + "PROV_INFO_JSON": [], + "PROVIDER_NAME": "Oregon Health & Science University", + } + + result = tin_npi_funcs.add_group_and_other(one_to_one_results, "test.txt") + + assert len(result["PROV_INFO_JSON"]) == 1 + entry = result["PROV_INFO_JSON"][0] + assert entry == { + "TIN": "N/A", + "NPI": "N/A", + "NAME": "Oregon Health & Science University", + "FROM_FILENAME": "N", + "IS_GROUP": "Y", + } + assert result["PROV_GROUP_NAME_FULL"] == ["Oregon Health & Science University"] + assert result["PROV_GROUP_TIN"] == [] + assert result["PROV_GROUP_NPI"] == [] + assert result["PROV_OTHER_TIN"] == [] + assert result["PROV_OTHER_NPI"] == [] + assert result["PROV_OTHER_NAME_FULL"] == [] + + def test_add_group_fallback_when_empty_and_name_list(self): + """List PROVIDER_NAME produces one synthesized entry per non-empty name.""" + one_to_one_results = { + "PROV_INFO_JSON": [], + "PROVIDER_NAME": ["OHSU", "OHSU Home Infusion Pharmacy"], + } + + result = tin_npi_funcs.add_group_and_other(one_to_one_results, "test.txt") + + assert len(result["PROV_INFO_JSON"]) == 2 + names = [e["NAME"] for e in result["PROV_INFO_JSON"]] + assert names == ["OHSU", "OHSU Home Infusion Pharmacy"] + for entry in result["PROV_INFO_JSON"]: + assert entry["TIN"] == "N/A" + assert entry["NPI"] == "N/A" + assert entry["IS_GROUP"] == "Y" + assert entry["FROM_FILENAME"] == "N" + assert result["PROV_GROUP_NAME_FULL"] == [ + "OHSU", + "OHSU Home Infusion Pharmacy", + ] + + @patch( + "src.pipelines.saas.prompts.prompt_calls.provider_name_match_check", + return_value=True, + ) + def test_add_group_no_fallback_when_prov_info_already_populated(self, _mock_check): + """When PROV_INFO_JSON already has entries, the fallback must not fire.""" + one_to_one_results = { + "PROV_INFO_JSON": [ + { + "TIN": "262998718", + "NPI": "1073054714", + "NAME": "OHSU", + "FROM_FILENAME": "N", + } + ], + "PROVIDER_NAME": "OHSU", + } + + result = tin_npi_funcs.add_group_and_other(one_to_one_results, "test.txt") + + # No synthesized entry: the single original entry remains, and the + # normal loop processed it (IS_GROUP now set). + assert len(result["PROV_INFO_JSON"]) == 1 + assert result["PROV_INFO_JSON"][0]["TIN"] == "262998718" + assert result["PROV_INFO_JSON"][0]["NPI"] == "1073054714" + assert result["PROV_INFO_JSON"][0]["IS_GROUP"] == "Y" + + @pytest.mark.parametrize("empty_name", ["", None, "N/A", []]) + def test_add_group_no_fallback_when_name_empty(self, empty_name): + """Empty/None/N/A/[] PROVIDER_NAME leaves PROV_INFO_JSON empty.""" + one_to_one_results = { + "PROV_INFO_JSON": [], + "PROVIDER_NAME": empty_name, + } + + result = tin_npi_funcs.add_group_and_other(one_to_one_results, "test.txt") + + assert result["PROV_INFO_JSON"] == [] + assert result["PROV_GROUP_NAME_FULL"] == [] + assert result["PROV_GROUP_TIN"] == [] + assert result["PROV_GROUP_NPI"] == [] + + def test_add_group_no_fallback_when_name_list_all_empty(self): + """List of only empty/N/A names leaves PROV_INFO_JSON empty.""" + one_to_one_results = { + "PROV_INFO_JSON": [], + "PROVIDER_NAME": ["", "N/A", None], + } + + result = tin_npi_funcs.add_group_and_other(one_to_one_results, "test.txt") + + assert result["PROV_INFO_JSON"] == [] + assert result["PROV_GROUP_NAME_FULL"] == []