863c9db9a1
Feature/code optimization
* Ran Black
* made a small change in code_last_check, fixed so it returns string and not single char
* Made changes to make sure that default_ind postprocess only happens to the cc output and not dashboard
* Merge bugfix/default_ind_postprocess into feature/code_optimization
- Parent-child: child_rank column init and cols_to_keep filter
- Postprocess: default_ind only on cc output
- code_last_check: parser str/list return handling + exception logging
- Prompt: FIELD ASSIGNMENT for code extraction (no CRITICAL prefix)
* More prompt changes and debugging print statements
* Added system to group same service term + bill type cd + claim type cd and then parallelize the code extraction (will improve consistency)
* Revert one-time PROV_INFO_JSON ad hoc logic; retain json_utils and output format
- Remove temporary postprocess step that filled empty TIN in PROV_INFO_JSON from
FILENAME_TIN (fill_prov_info_tin_from_filename_tin call in postprocess.py).
This behavior is intended to move upstream per JIRA (tin_npi_funcs / extraction).
- Revert postprocess_existing_output.py to generic config: empty INPUT_DIR,
INPUT_FILENAME, OUTPUT_CSV. Remove one-time hyphen-strip for TIN/NPI in
PROV_INFO_JSON and remove _strip_hyphens_from_prov_info_json_cell.
- Keep json_utils PROV_INFO_JSON helpers (parse_prov_info_json_cell,
serialize_prov_info_json, format_prov_info_json) and (str,str) serialization
in contract_config_postprocess. Keep fill_prov_info_tin_from_filename_tin and
related helpers in postprocessing_funcs for potential upstream reuse.
- Tests in test_json_parsers.py updated for (str,str) PROV_INFO_JSON output.
* Add arbitration-based implicit code flow and prefer Level 2 RAG (Fix 5d, 4c+7)
Implicit flow no longer uses first-match-wins. We build a case from all
applicable strategies and run a single arbitration step to choose the
best candidate.
- build_implicit_candidates: Run Category (when explicit had Category),
Special, and RAG; return list of {source, code_answer_dict}. RAG
result uses preferred level when both return (Fix 5d).
- code_implicit_rag (Fix 5d): Run both Level 1 and Level 2, collect
results; when both return non-empty, prefer Level 2 over Level 1.
- code_implicit_arbitration: 0 candidates -> None; 1 candidate -> return
it with CODE_METHODOLOGY = 'Implicit - Arbitration (Source)' (no LLM);
2+ candidates -> one CODE_IMPLICIT_ARBITRATION LLM call (chosen_index or
no_match). If no candidates or no_match, code_last_check runs as before.
- prompt_templates: Add CODE_IMPLICIT_ARBITRATION and
CODE_IMPLICIT_ARBITRATION_INSTRUCTION (cached). Use
_create_json_dict_parser(field_names=['chose…
* fix: test_code_breakout avoid LLM call and flaky order on CI
- Add BILL_TYPE_CD_DESC to test data so pre-pass skips fill_bill_type (no Bedrock on CI)
- Use side_effect function keyed by SERVICE_TERM for deterministic result under ThreadPoolExecutor
* ran black
* feat: hybrid code validation with unmatched tagging and code quality fixes
Validation and unmatched tagging:
- Add format-only filtering for explicit codes (drop wrong-format, preserve valid)
- Add unified _has_unmatched_codes() used by both explicit and implicit paths
- Tag CODE_METHODOLOGY with "- Unmatched" when codes are format-valid but
not explicit keys in our mappings (codes and range keys count as mapped)
- Remove retry logic and "Explicit - Validation Failed" in favor of single
LLM call with Unmatched tagging
- Include all revenue level mappings in valid revenue set
Code quality and bug fixes:
- Fix fill_bill_type += string bug (was splitting "11X" into chars)
- Fix bare except clauses in code_explicit, code_category,
code_implicit_special, fill_bill_type (now except Exception as e)
- Replace eval() with json.loads() in fill_grouper_cd_desc
- Implement INVALID_SERVICE short-circuit in RAG (Fix 5a)
- Cache valid procedure/revenue code sets per Constants instance
- Store mappings in level_dicts…
* chore: remove debug print statements from code extraction pipeline
Remove all [DEBUG_CODE_EXTRACTION] print statements added during
development of the hybrid validation and implicit arbitration flow.
* refactor: robust PROV_INFO_JSON sanitization with multi-format parsing
Replace the simple format_prov_info_json with a layered
sanitize_prov_info_json that handles all known malformed variants:
single-quoted dicts, empty-value-after-colon patterns, list-typed
field values, and hyphenated TINs. Extract _normalize_prov_entries
and _prov_value_to_str helpers for uniform str-valued output.
format_prov_info_json now delegates to sanitize_prov_info_json.
* merge: sync feature/code_optimization with DEV
Resolve conflict in prompt_templates.py: keep detailed CRITICAL field
assignment instructions from feature branch; accept new
AARETE_DERIVED_PROVIDER_NAME prompt function from DEV.
* Minor change to explict prompt
* fix: resolve mypy errors and remove stale pipe-delim splits in RAG
- Narrow grouper_cd type to str|None with explicit None check to
satisfy mypy in fill_grouper_cd_desc.
- Remove dead pipe-delim split logic from CPT and HCPCS matching
blocks (no pipes in those mappings).
- Retain pipe split for revenue block only (rev_level1.csv keys are
pipe-delimited, e.g. "0810|0811|0812").
* Code extraction: format-only explicit, keep unmapped tag, RAG Level 1/2 as separate candidates
- Explicit: accept all format-valid codes (single + range); do not drop for mapping.
Validity check only appends "- Unmatched" to methodology when code not in mappings.
- Add _procedure_code_or_range_format_valid; use in validate_explicit_codes and _has_unmatched_codes.
- RAG: return list of Level 1 and Level 2 candidates (distinct source keys); remove Level 2 preference.
- build_implicit_candidates: consume RAG list and append each as separate candidate.
- CODE_EXPLICIT: clarify range extraction when service term describes a range.
- CODE_IMPLICIT_ARBITRATION: choose most appropriate candidate(s), avoid overly broad.
- Tests updated for new RAG return shape and validate_explicit_codes behavior.
* Resolved Merge Conflicts
* Fixed failing tests
* Remove unused functions
* Remove unused functions
* Revert "Remove unused functions"
This reverts commit 39954db6a421e55501282d4e6a270c88624049fe.
* remove unused
* Merged DEV into feature/code_optimization
* Merged DEV into feature/code_optimization
Approved-by: Katon Minhas
1009 lines
43 KiB
Python
1009 lines
43 KiB
Python
import json
|
|
import re
|
|
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pandas as pd
|
|
|
|
import src.codes.code_funcs as code_funcs
|
|
import src.prompts.prompt_templates as prompt_templates
|
|
from src.constants.constants import Constants
|
|
from src.constants.delimiters import Delimiter
|
|
|
|
|
|
class TestCodeFuncs(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures before each test method."""
|
|
# Clear the module-level valid code set caches between tests.
|
|
code_funcs._VALID_PROC_CACHE.clear()
|
|
code_funcs._VALID_REV_CACHE.clear()
|
|
|
|
self.constants = MagicMock(spec=Constants)
|
|
|
|
# Configure mock constants
|
|
self.constants.SYNONYM_MAP = {
|
|
"DME": "DURABLE MEDICAL EQUIPMENT",
|
|
"SNF": "SKILLED NURSING FACILITY",
|
|
}
|
|
self.constants.REMOVAL_LIST = ["INPATIENT", "OUTPATIENT", "SERVICES"]
|
|
self.constants.STOP_WORD_LIST = ["THE", "AND", "OF"]
|
|
self.constants.DO_NOT_RUN = ["BY REPORT", "STOP-LOSS"]
|
|
|
|
# Setup mappings
|
|
self.constants.CPT_LEVEL1_MAPPING = {"12345": "Test Procedure"}
|
|
self.constants.HCPCS_LEVEL1_MAPPING = {"J0001": "Test Drug"}
|
|
self.constants.REV_LEVEL1_MAPPING = {"123": "Test Revenue"}
|
|
|
|
self.constants.CPT_LEVEL2_MAPPING = {"67890": "Advanced Procedure"}
|
|
self.constants.HCPCS_LEVEL2_MAPPING = {"J0002": "Advanced Drug"}
|
|
self.constants.REV_MAPPING = {"0456": "Advanced Revenue"}
|
|
# Full mappings for validation and description fill (Fix 1 / Fix 2)
|
|
self.constants.CPT_FULL_MAPPING = {
|
|
"12345": "Test Procedure",
|
|
"99213": "Office visit, established patient",
|
|
}
|
|
self.constants.HCPCS_FULL_MAPPING = {
|
|
"J0001": "Test Drug",
|
|
"G0525": "Care management",
|
|
}
|
|
|
|
# Bill type mappings matching the actual JSON structure
|
|
self.constants.BILL_TYPE_MAPPING = {
|
|
"011X": "Inpatient Hospital",
|
|
"012X": "Inpatient Hospital",
|
|
"018X": "Inpatient Hospital",
|
|
}
|
|
# Reverse mapping returns a list of codes for each description
|
|
self.constants.BILL_TYPE_REVERSE_MAPPING = {
|
|
"Inpatient Hospital": ["011X", "012X", "018X"]
|
|
}
|
|
|
|
# Mock the embedding model and function
|
|
mock_embedding = MagicMock()
|
|
mock_embedding.index.search.return_value = ([0.9], [[0]])
|
|
mock_embedding.choices = ["Test Description"]
|
|
self.constants.get_embedding.return_value = mock_embedding
|
|
|
|
self.constants.EMBEDDING_MODEL = MagicMock()
|
|
mock_encoded = MagicMock()
|
|
mock_encoded.astype.return_value = "mock_vector"
|
|
self.constants.EMBEDDING_MODEL.encode.return_value = mock_encoded
|
|
|
|
def tearDown(self):
|
|
"""Clear prompt_templates constants cache so a MagicMock is not reused by other tests."""
|
|
prompt_templates.set_constants_for_instruction_cache(None)
|
|
|
|
def test_clean_service(self):
|
|
"""Test the clean_service function with various inputs."""
|
|
# Test empty input
|
|
self.assertEqual(code_funcs.clean_service("", self.constants), "")
|
|
self.assertEqual(code_funcs.clean_service(None, self.constants), "")
|
|
|
|
# Test synonym mapping
|
|
self.assertEqual(
|
|
code_funcs.clean_service("DME", self.constants), "DURABLE MEDICAL EQUIPMENT"
|
|
)
|
|
|
|
# Test removal list
|
|
self.assertEqual(
|
|
code_funcs.clean_service("INPATIENT SERVICES", self.constants), ""
|
|
)
|
|
|
|
# Test stop words only
|
|
self.assertEqual(code_funcs.clean_service("THE AND OF", self.constants), "")
|
|
|
|
# Test unlisted service
|
|
self.assertEqual(
|
|
code_funcs.clean_service("UNLISTED PROCEDURE", self.constants), "UNLISTED"
|
|
)
|
|
|
|
# Test normal case
|
|
self.assertEqual(
|
|
code_funcs.clean_service("CT SCAN SNF", self.constants),
|
|
"CT SCAN SKILLED NURSING FACILITY",
|
|
)
|
|
|
|
@patch("src.utils.llm_utils.invoke_claude")
|
|
@patch("src.utils.string_utils.universal_json_load")
|
|
def test_code_explicit(self, mock_json_load, mock_invoke_claude):
|
|
"""Test the code_explicit function for extracting explicit codes."""
|
|
# Setup mocks
|
|
mock_invoke_claude.return_value = '{"PROCEDURE_CD": "12345"}'
|
|
mock_json_load.return_value = {"PROCEDURE_CD": "12345"}
|
|
|
|
# Test successful extraction
|
|
result = code_funcs.code_explicit(
|
|
"TEST SERVICE", "TEST_METHODOLOGY", "test.pdf"
|
|
)
|
|
self.assertEqual(result, {"PROCEDURE_CD": "12345"})
|
|
mock_invoke_claude.assert_called_once()
|
|
|
|
@patch("src.utils.llm_utils.invoke_claude")
|
|
@patch("src.utils.string_utils.universal_json_load")
|
|
def test_code_category(self, mock_json_load, mock_invoke_claude):
|
|
"""Test the code_category function for detecting code categories."""
|
|
# Setup mocks
|
|
mock_invoke_claude.return_value = '["Drug A", "Drug B"]'
|
|
mock_json_load.return_value = ["Drug A", "Drug B"]
|
|
|
|
# Setup test data
|
|
proc_category = ["Category: J"]
|
|
hcpcs_mapping = {"J0001": "Drug A", "J0002": "Drug B", "K0001": "Other Drug"}
|
|
|
|
# Test category matching
|
|
result = code_funcs.code_category(
|
|
"J CODES", proc_category, hcpcs_mapping, "test.pdf"
|
|
)
|
|
|
|
# Verify the results - code_category returns lists (JSON conversion happens in extract_codes_from_service)
|
|
self.assertEqual(result["PROCEDURE_CD"], ["J0001", "J0002"])
|
|
self.assertEqual(result["PROCEDURE_CD_DESC"], ["Drug A", "Drug B"])
|
|
self.assertIn("J0001", result["PROCEDURE_CD"])
|
|
self.assertIn("J0002", result["PROCEDURE_CD"])
|
|
self.assertIn("Drug A", result["PROCEDURE_CD_DESC"])
|
|
self.assertIn("Drug B", result["PROCEDURE_CD_DESC"])
|
|
|
|
@patch("src.utils.llm_utils.invoke_claude")
|
|
def test_code_implicit_special(self, mock_invoke_claude):
|
|
"""Test the code_implicit_special function for special case handling."""
|
|
# Setup mocks - parser returns a list
|
|
mock_invoke_claude.return_value = '["Drugs"]'
|
|
|
|
# Test drug category - returns a list
|
|
result = code_funcs.code_implicit_special("DRUG SERVICE", "test.pdf")
|
|
self.assertEqual(result["PROCEDURE_CD"], ["J0000-J9999"])
|
|
self.assertEqual(result["PROCEDURE_CD_DESC"], ["Drugs"])
|
|
|
|
# Test no match - parser returns empty list
|
|
mock_invoke_claude.return_value = "[]"
|
|
result = code_funcs.code_implicit_special("OTHER SERVICE", "test.pdf")
|
|
self.assertEqual(result, {})
|
|
|
|
def test_get_embedding_levels(self):
|
|
"""Test the get_embedding_levels function for determining embedding levels."""
|
|
# Test level 1 with both codes enabled
|
|
implicit_run_dict = {"PROCEDURE_CD": True, "REVENUE_CD": True}
|
|
embedding_levels, cpt_mapping, hcpcs_mapping, rev_mapping = (
|
|
code_funcs.get_embedding_levels(1, implicit_run_dict, self.constants)
|
|
)
|
|
|
|
self.assertIn("cpt_level1", embedding_levels)
|
|
self.assertIn("hcpcs_level1", embedding_levels)
|
|
self.assertIn("rev_level1", embedding_levels)
|
|
self.assertEqual(cpt_mapping, self.constants.CPT_LEVEL1_MAPPING)
|
|
|
|
# Test level 2 with only procedure code enabled
|
|
implicit_run_dict = {"PROCEDURE_CD": True, "REVENUE_CD": False}
|
|
embedding_levels, cpt_mapping, hcpcs_mapping, rev_mapping = (
|
|
code_funcs.get_embedding_levels(2, implicit_run_dict, self.constants)
|
|
)
|
|
|
|
self.assertIn("cpt_level2", embedding_levels)
|
|
self.assertIn("hcpcs_level2", embedding_levels)
|
|
self.assertNotIn("rev", embedding_levels)
|
|
self.assertEqual(cpt_mapping, self.constants.CPT_LEVEL2_MAPPING)
|
|
|
|
def test_get_match_list(self):
|
|
"""Test the get_match_list function for retrieving matches from embeddings."""
|
|
# Configure mock for proper array-like return values
|
|
mock_embedding = MagicMock()
|
|
# Search returns a tuple of (scores, indices) where each is a 2D array
|
|
mock_embedding.index.search.return_value = (
|
|
# Scores array - 2D array with shape [1, top_k]
|
|
[[0.9]],
|
|
# Indices array - 2D array with shape [1, top_k]
|
|
[[0]],
|
|
)
|
|
mock_embedding.choices = ["Test Description"]
|
|
|
|
# Replace the mock embedding in constants for this test
|
|
self.constants.get_embedding.return_value = mock_embedding
|
|
|
|
# Test with constants mock configured in setUp
|
|
match_list, highest_similarity = code_funcs.get_match_list(
|
|
"TEST SERVICE", ["cpt_level1"], self.constants, 1
|
|
)
|
|
|
|
# Verify results
|
|
self.assertEqual(match_list, ["Test Description"])
|
|
self.assertEqual(highest_similarity, 0.9)
|
|
|
|
# Verify the appropriate method calls
|
|
self.constants.EMBEDDING_MODEL.encode.assert_called_once()
|
|
self.constants.get_embedding.assert_called_once_with("cpt_level1")
|
|
|
|
@patch("src.utils.llm_utils.invoke_claude")
|
|
@patch("src.utils.string_utils.universal_json_load")
|
|
@patch("src.codes.code_funcs.get_match_list")
|
|
def test_code_implicit_rag(
|
|
self, mock_get_match_list, mock_json_load, mock_invoke_claude
|
|
):
|
|
"""Test the code_implicit_rag function for RAG-based code extraction."""
|
|
# Mock get_match_list to return properly formatted results
|
|
mock_get_match_list.return_value = (["Test Procedure"], 0.9)
|
|
|
|
# Setup mocks for Claude responses
|
|
mock_invoke_claude.return_value = '["Test Procedure"]'
|
|
mock_json_load.return_value = ["Test Procedure"]
|
|
|
|
# Test with successful match
|
|
implicit_run_dict = {"PROCEDURE_CD": True, "REVENUE_CD": True}
|
|
|
|
# Patch the get_embedding_levels function to return predictable values
|
|
with patch(
|
|
"src.codes.code_funcs.get_embedding_levels"
|
|
) as mock_get_embedding_levels:
|
|
# Configure mock to return appropriate values for embedding levels
|
|
mock_get_embedding_levels.return_value = (
|
|
["cpt_level1"],
|
|
{"12345": "Test Procedure"},
|
|
{},
|
|
{},
|
|
)
|
|
|
|
# Call the function
|
|
result = code_funcs.code_implicit_rag(
|
|
"TEST SERVICE", implicit_run_dict, "test.pdf", self.constants
|
|
)
|
|
|
|
# Verify results - code_implicit_rag returns list of candidates with source Level 1/Level 2
|
|
self.assertIsInstance(result, list)
|
|
self.assertGreaterEqual(len(result), 1)
|
|
codes_found = False
|
|
for item in result:
|
|
self.assertIn("source", item)
|
|
self.assertIn("code_answer_dict", item)
|
|
d = item["code_answer_dict"]
|
|
if d.get("PROCEDURE_CD") == ["12345"]:
|
|
codes_found = True
|
|
self.assertEqual(d["PROCEDURE_CD_DESC"], ["Test Procedure"])
|
|
break
|
|
self.assertTrue(
|
|
codes_found, "Expected PROCEDURE_CD ['12345'] in a candidate"
|
|
)
|
|
|
|
# Test exception handling - returns list (possibly empty)
|
|
mock_json_load.side_effect = Exception("Test error")
|
|
result = code_funcs.code_implicit_rag(
|
|
"TEST SERVICE", implicit_run_dict, "test.pdf", self.constants
|
|
)
|
|
self.assertIsInstance(result, list)
|
|
|
|
@patch("src.utils.llm_utils.invoke_claude")
|
|
@patch("src.utils.string_utils.universal_json_load")
|
|
@patch("src.codes.code_funcs.get_match_list")
|
|
def test_code_implicit_rag_invalid_service_short_circuits(
|
|
self, mock_get_match_list, mock_json_load, mock_invoke_claude
|
|
):
|
|
"""INVALID_SERVICE in RAG response is a hard stop: no codes added."""
|
|
mock_get_match_list.return_value = (["Test Procedure"], 0.9)
|
|
mock_invoke_claude.return_value = '["INVALID_SERVICE", "Test Procedure"]'
|
|
mock_json_load.return_value = ["INVALID_SERVICE", "Test Procedure"]
|
|
implicit_run_dict = {"PROCEDURE_CD": True, "REVENUE_CD": True}
|
|
|
|
with patch(
|
|
"src.codes.code_funcs.get_embedding_levels"
|
|
) as mock_get_embedding_levels:
|
|
mock_get_embedding_levels.return_value = (
|
|
["cpt_level1"],
|
|
{"12345": "Test Procedure"},
|
|
{},
|
|
{},
|
|
)
|
|
result = code_funcs.code_implicit_rag(
|
|
"TEST SERVICE", implicit_run_dict, "test.pdf", self.constants
|
|
)
|
|
|
|
# INVALID_SERVICE yields no candidates with codes
|
|
self.assertIsInstance(result, list)
|
|
self.assertEqual(len(result), 0)
|
|
|
|
@patch("src.utils.llm_utils.invoke_claude")
|
|
def test_fill_bill_type_list_reverse_mapping(self, mock_invoke_claude):
|
|
"""fill_bill_type handles list values in BILL_TYPE_REVERSE_MAPPING."""
|
|
mock_invoke_claude.return_value = '["Inpatient Hospital"]'
|
|
reverse_mapping = {"Inpatient Hospital": ["11X", "12X"]}
|
|
answer_dict = {}
|
|
result = code_funcs.fill_bill_type(
|
|
"INPATIENT SERVICE",
|
|
answer_dict,
|
|
self.constants.BILL_TYPE_MAPPING,
|
|
reverse_mapping,
|
|
)
|
|
self.assertEqual(result["BILL_TYPE_CD"], ["11X", "12X"])
|
|
|
|
@patch("src.utils.llm_utils.invoke_claude")
|
|
def test_code_last_check(self, mock_invoke_claude):
|
|
"""Test the code_last_check function for categorizing non-matched services."""
|
|
# Parser is format-aware and returns 'Specific' or 'Generic' directly.
|
|
# Test specific case
|
|
mock_invoke_claude.return_value = '["Specific"]'
|
|
result = code_funcs.code_last_check("SPECIAL SERVICE", "test.pdf")
|
|
self.assertEqual(result, "Specific")
|
|
|
|
# Test generic case
|
|
mock_invoke_claude.return_value = '["Generic"]'
|
|
result = code_funcs.code_last_check("GENERIC SERVICE", "test.pdf")
|
|
self.assertEqual(result, "Generic")
|
|
|
|
def test_format_implicit_candidate_for_prompt(self):
|
|
"""Test _format_implicit_candidate_for_prompt formats a candidate for arbitration."""
|
|
candidate = {
|
|
"source": "Category",
|
|
"code_answer_dict": {
|
|
"PROCEDURE_CD": ["J0001", "J0002"],
|
|
"PROCEDURE_CD_DESC": ["Drug A", "Drug B"],
|
|
},
|
|
}
|
|
result = code_funcs._format_implicit_candidate_for_prompt(candidate, 0)
|
|
self.assertIn("Candidate 0 (Source: Category)", result)
|
|
self.assertIn("PROCEDURE_CD", result)
|
|
self.assertIn("J0001", result)
|
|
|
|
@patch("src.codes.code_funcs.code_implicit_rag")
|
|
@patch("src.codes.code_funcs.code_implicit_special")
|
|
@patch("src.codes.code_funcs.code_category")
|
|
def test_build_implicit_candidates_empty_when_none_return(
|
|
self, mock_category, mock_special, mock_rag
|
|
):
|
|
"""Test build_implicit_candidates returns empty list when no strategy returns."""
|
|
mock_category.return_value = {}
|
|
mock_special.return_value = {}
|
|
mock_rag.return_value = [] # RAG returns list of candidates
|
|
implicit_run_dict = {"PROCEDURE_CD": True, "REVENUE_CD": True}
|
|
result = code_funcs.build_implicit_candidates(
|
|
"SERVICE",
|
|
{},
|
|
implicit_run_dict,
|
|
"test.pdf",
|
|
self.constants,
|
|
)
|
|
self.assertEqual(result, [])
|
|
|
|
@patch("src.codes.code_funcs.code_implicit_rag")
|
|
@patch("src.codes.code_funcs.code_implicit_special")
|
|
@patch("src.codes.code_funcs.code_category")
|
|
def test_build_implicit_candidates_category_when_explicit_has_category(
|
|
self, mock_category, mock_special, mock_rag
|
|
):
|
|
"""Test build_implicit_candidates runs category and adds candidate when explicit had Category."""
|
|
mock_category.return_value = {
|
|
"PROCEDURE_CD": ["J0001"],
|
|
"PROCEDURE_CD_DESC": ["Drug A"],
|
|
}
|
|
mock_special.return_value = {}
|
|
mock_rag.return_value = [] # RAG returns list of candidates
|
|
implicit_run_dict = {"PROCEDURE_CD": True, "REVENUE_CD": True}
|
|
code_after_explicit = {"PROCEDURE_CD": ["Category: J"]}
|
|
result = code_funcs.build_implicit_candidates(
|
|
"J CODES",
|
|
code_after_explicit,
|
|
implicit_run_dict,
|
|
"test.pdf",
|
|
self.constants,
|
|
)
|
|
self.assertEqual(len(result), 1)
|
|
self.assertEqual(result[0]["source"], "Category")
|
|
self.assertEqual(result[0]["code_answer_dict"]["PROCEDURE_CD"], ["J0001"])
|
|
mock_category.assert_called_once()
|
|
|
|
@patch("src.codes.code_funcs.code_implicit_rag")
|
|
@patch("src.codes.code_funcs.code_implicit_special")
|
|
@patch("src.codes.code_funcs.code_category")
|
|
def test_build_implicit_candidates_skips_category_without_category_in_explicit(
|
|
self, mock_category, mock_special, mock_rag
|
|
):
|
|
"""Test build_implicit_candidates does not run category when explicit has no Category."""
|
|
mock_special.return_value = {
|
|
"PROCEDURE_CD": ["J0000-J9999"],
|
|
"PROCEDURE_CD_DESC": ["Drugs"],
|
|
}
|
|
mock_rag.return_value = [] # RAG returns list of candidates
|
|
implicit_run_dict = {"PROCEDURE_CD": True, "REVENUE_CD": True}
|
|
result = code_funcs.build_implicit_candidates(
|
|
"DRUG SERVICE",
|
|
{},
|
|
implicit_run_dict,
|
|
"test.pdf",
|
|
self.constants,
|
|
)
|
|
mock_category.assert_not_called()
|
|
self.assertEqual(len(result), 1)
|
|
self.assertEqual(result[0]["source"], "Special")
|
|
|
|
@patch("src.codes.code_funcs.code_implicit_rag")
|
|
@patch("src.codes.code_funcs.code_implicit_special")
|
|
@patch("src.codes.code_funcs.code_category")
|
|
def test_build_implicit_candidates_collects_category_special_rag(
|
|
self, mock_category, mock_special, mock_rag
|
|
):
|
|
"""Test build_implicit_candidates collects candidates from all three when all return."""
|
|
mock_category.return_value = {
|
|
"PROCEDURE_CD": ["A0000-A9999"],
|
|
"PROCEDURE_CD_DESC": ["A range"],
|
|
}
|
|
mock_special.return_value = {
|
|
"PROCEDURE_CD": ["J0000-J9999"],
|
|
"PROCEDURE_CD_DESC": ["Drugs"],
|
|
}
|
|
mock_rag.return_value = [
|
|
{
|
|
"source": "Level 2",
|
|
"code_answer_dict": {
|
|
"PROCEDURE_CD": ["99213"],
|
|
"PROCEDURE_CD_DESC": ["Office visit"],
|
|
"CODE_METHODOLOGY": "Implicit - Level 2",
|
|
},
|
|
},
|
|
]
|
|
implicit_run_dict = {"PROCEDURE_CD": True, "REVENUE_CD": True}
|
|
code_after_explicit = {"PROCEDURE_CD": ["Category: A"]}
|
|
result = code_funcs.build_implicit_candidates(
|
|
"SERVICE",
|
|
code_after_explicit,
|
|
implicit_run_dict,
|
|
"test.pdf",
|
|
self.constants,
|
|
)
|
|
self.assertEqual(len(result), 3)
|
|
sources = [c["source"] for c in result]
|
|
self.assertIn("Category", sources)
|
|
self.assertIn("Special", sources)
|
|
self.assertIn("Level 2", sources)
|
|
|
|
def test_code_implicit_arbitration_zero_candidates_returns_none(self):
|
|
"""Test code_implicit_arbitration returns None when candidates list is empty."""
|
|
result = code_funcs.code_implicit_arbitration("SERVICE", [], "test.pdf")
|
|
self.assertIsNone(result)
|
|
|
|
def test_code_implicit_arbitration_single_candidate_returns_it_no_llm(self):
|
|
"""Test code_implicit_arbitration with one candidate returns it without calling LLM."""
|
|
candidates = [
|
|
{
|
|
"source": "Category",
|
|
"code_answer_dict": {
|
|
"PROCEDURE_CD": ["J0001"],
|
|
"PROCEDURE_CD_DESC": ["Drug A"],
|
|
},
|
|
}
|
|
]
|
|
result = code_funcs.code_implicit_arbitration("J CODES", candidates, "test.pdf")
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result["PROCEDURE_CD"], ["J0001"])
|
|
self.assertEqual(
|
|
result["CODE_METHODOLOGY"],
|
|
"Implicit - Arbitration (Category)",
|
|
)
|
|
|
|
@patch("src.utils.llm_utils.invoke_claude")
|
|
def test_code_implicit_arbitration_multiple_candidates_uses_chosen_index(
|
|
self, mock_invoke_claude
|
|
):
|
|
"""Test code_implicit_arbitration with 2+ candidates calls LLM and returns chosen candidate."""
|
|
mock_invoke_claude.return_value = '{"chosen_index": 1}'
|
|
candidates = [
|
|
{
|
|
"source": "Category",
|
|
"code_answer_dict": {"PROCEDURE_CD": ["A0000-A9999"]},
|
|
},
|
|
{"source": "Level 2", "code_answer_dict": {"PROCEDURE_CD": ["99213"]}},
|
|
]
|
|
result = code_funcs.code_implicit_arbitration(
|
|
"Office visit", candidates, "test.pdf"
|
|
)
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result["PROCEDURE_CD"], ["99213"])
|
|
self.assertEqual(
|
|
result["CODE_METHODOLOGY"],
|
|
"Implicit - Arbitration (Level 2)",
|
|
)
|
|
mock_invoke_claude.assert_called_once()
|
|
|
|
@patch("src.utils.llm_utils.invoke_claude")
|
|
def test_code_implicit_arbitration_no_match_returns_none(self, mock_invoke_claude):
|
|
"""Test code_implicit_arbitration returns None when LLM returns no_match."""
|
|
mock_invoke_claude.return_value = '{"no_match": true}'
|
|
candidates = [
|
|
{
|
|
"source": "Category",
|
|
"code_answer_dict": {"PROCEDURE_CD": ["A0000-A9999"]},
|
|
},
|
|
{
|
|
"source": "Special",
|
|
"code_answer_dict": {"PROCEDURE_CD": ["10004-69990"]},
|
|
},
|
|
]
|
|
result = code_funcs.code_implicit_arbitration(
|
|
"Unclear service", candidates, "test.pdf"
|
|
)
|
|
self.assertIsNone(result)
|
|
|
|
@patch("src.utils.llm_utils.invoke_claude")
|
|
def test_code_implicit_arbitration_invalid_index_fallback_to_zero(
|
|
self, mock_invoke_claude
|
|
):
|
|
"""Test code_implicit_arbitration falls back to index 0 when chosen_index out of range."""
|
|
mock_invoke_claude.return_value = '{"chosen_index": 99}'
|
|
candidates = [
|
|
{"source": "Category", "code_answer_dict": {"PROCEDURE_CD": ["J0001"]}},
|
|
]
|
|
result = code_funcs.code_implicit_arbitration("SERVICE", candidates, "test.pdf")
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(result["PROCEDURE_CD"], ["J0001"])
|
|
self.assertEqual(
|
|
result["CODE_METHODOLOGY"],
|
|
"Implicit - Arbitration (Category)",
|
|
)
|
|
|
|
@patch("src.utils.llm_utils.invoke_claude")
|
|
def test_fill_bill_type(self, mock_invoke_claude):
|
|
"""Test the fill_bill_type function for populating bill type information."""
|
|
mock_invoke_claude.return_value = '["Inpatient Hospital"]'
|
|
|
|
# Test with valid bill type
|
|
answer_dict = {}
|
|
result = code_funcs.fill_bill_type(
|
|
"INPATIENT SERVICE",
|
|
answer_dict,
|
|
self.constants.BILL_TYPE_MAPPING,
|
|
self.constants.BILL_TYPE_REVERSE_MAPPING,
|
|
)
|
|
|
|
# Should return all codes for "Inpatient Hospital" description
|
|
# Reverse mapping returns ["011X", "012X", "018X"] for "Inpatient Hospital"
|
|
self.assertEqual(result["BILL_TYPE_CD"], ["011X", "012X", "018X"])
|
|
self.assertEqual(result["BILL_TYPE_CD_DESC"], ["Inpatient Hospital"])
|
|
|
|
# Test with empty response
|
|
mock_invoke_claude.return_value = "[]"
|
|
answer_dict = {"SERVICE_TERM": "Test"}
|
|
result = code_funcs.fill_bill_type(
|
|
"OTHER SERVICE",
|
|
answer_dict,
|
|
self.constants.BILL_TYPE_MAPPING,
|
|
self.constants.BILL_TYPE_REVERSE_MAPPING,
|
|
)
|
|
|
|
self.assertEqual(result, {"SERVICE_TERM": "Test"})
|
|
|
|
def test_get_implicit_runs(self):
|
|
"""Test the get_implicit_runs function for determining which code runs to execute."""
|
|
# Test hospital claim type
|
|
result = code_funcs.get_implicit_runs({"AARETE_DERIVED_CLAIM_TYPE_CD": "H"})
|
|
self.assertTrue(result["REVENUE_CD"])
|
|
|
|
# Test medical claim type with bill type
|
|
result = code_funcs.get_implicit_runs(
|
|
{"AARETE_DERIVED_CLAIM_TYPE_CD": "M", "BILL_TYPE_CD_DESC": "Outpatient"}
|
|
)
|
|
self.assertTrue(result["REVENUE_CD"])
|
|
self.assertTrue(result["PROCEDURE_CD"])
|
|
|
|
# Test inpatient bill type
|
|
result = code_funcs.get_implicit_runs(
|
|
{
|
|
"AARETE_DERIVED_CLAIM_TYPE_CD": "M",
|
|
"BILL_TYPE_CD_DESC": "Inpatient Hospital",
|
|
}
|
|
)
|
|
self.assertTrue(result["REVENUE_CD"])
|
|
self.assertFalse(result["PROCEDURE_CD"])
|
|
|
|
@patch("src.codes.code_funcs.clean_service")
|
|
@patch("src.codes.code_funcs.fill_bill_type")
|
|
@patch("src.codes.code_funcs.get_implicit_runs")
|
|
@patch("src.codes.code_funcs.code_explicit")
|
|
@patch("src.codes.code_funcs.code_category")
|
|
@patch("src.codes.code_funcs.code_implicit_special")
|
|
@patch("src.codes.code_funcs.code_implicit_rag")
|
|
@patch("src.codes.code_funcs.code_last_check")
|
|
def test_extract_codes_from_service(
|
|
self,
|
|
mock_last_check,
|
|
mock_implicit_rag,
|
|
mock_implicit_special,
|
|
mock_category,
|
|
mock_explicit,
|
|
mock_implicit_runs,
|
|
mock_fill_bill_type,
|
|
mock_clean_service,
|
|
):
|
|
"""Test the extract_codes_from_service function end-to-end."""
|
|
# Setup mocks
|
|
mock_clean_service.return_value = "CLEAN SERVICE"
|
|
mock_fill_bill_type.return_value = {
|
|
"SERVICE_TERM": "TEST SERVICE",
|
|
"BILL_TYPE_CD_DESC": "Outpatient",
|
|
}
|
|
mock_implicit_runs.return_value = {"PROCEDURE_CD": True, "REVENUE_CD": True}
|
|
|
|
# Test explicit code match - extract_codes_from_service normalizes to JSON list format
|
|
mock_explicit.return_value = {"PROCEDURE_CD": "12345"}
|
|
result = code_funcs.extract_codes_from_service(
|
|
{"SERVICE_TERM": "TEST SERVICE"}, self.constants
|
|
)
|
|
self.assertEqual(result["PROCEDURE_CD"], '["12345"]')
|
|
self.assertEqual(result["CODE_METHODOLOGY"], "Explicit")
|
|
|
|
# Test unlisted service
|
|
mock_clean_service.return_value = "UNLISTED"
|
|
result = code_funcs.extract_codes_from_service(
|
|
{"SERVICE_TERM": "UNLISTED SERVICE"}, self.constants
|
|
)
|
|
self.assertEqual(result["PROCEDURE_CD_DESC"], "UNLISTED")
|
|
|
|
# Test category match (arbitration with single candidate returns it)
|
|
mock_clean_service.return_value = "CLEAN SERVICE"
|
|
mock_explicit.return_value = {"PROCEDURE_CD": ["Category: J"]}
|
|
mock_category.return_value = {
|
|
"PROCEDURE_CD": ["J0001"],
|
|
"PROCEDURE_CD_DESC": ["Drug A"],
|
|
}
|
|
result = code_funcs.extract_codes_from_service(
|
|
{"SERVICE_TERM": "J CODES"}, self.constants
|
|
)
|
|
self.assertEqual(
|
|
result["CODE_METHODOLOGY"], "Implicit - Arbitration (Category)"
|
|
)
|
|
|
|
# Test special case match (arbitration with single candidate)
|
|
mock_explicit.return_value = {}
|
|
mock_implicit_special.return_value = {
|
|
"PROCEDURE_CD": "J0000-J9999",
|
|
"PROCEDURE_CD_DESC": "Drugs",
|
|
}
|
|
result = code_funcs.extract_codes_from_service(
|
|
{"SERVICE_TERM": "DRUG SERVICE"}, self.constants
|
|
)
|
|
# Range J0000-J9999 not in mock mapping so methodology gets - Unmatched
|
|
self.assertIn("Implicit - Arbitration (Special)", result["CODE_METHODOLOGY"])
|
|
self.assertIn("Unmatched", result["CODE_METHODOLOGY"])
|
|
|
|
# Test RAG match (arbitration with single candidate; RAG returns list with Level 1)
|
|
mock_implicit_special.return_value = {}
|
|
mock_implicit_rag.return_value = [
|
|
{
|
|
"source": "Level 1",
|
|
"code_answer_dict": {
|
|
"PROCEDURE_CD": "12345",
|
|
"CODE_METHODOLOGY": "Implicit - Level 1",
|
|
},
|
|
},
|
|
]
|
|
result = code_funcs.extract_codes_from_service(
|
|
{"SERVICE_TERM": "TEST SERVICE"}, self.constants
|
|
)
|
|
self.assertEqual(result["CODE_METHODOLOGY"], "Implicit - Arbitration (Level 1)")
|
|
|
|
# Test no match - specific
|
|
mock_implicit_rag.return_value = {}
|
|
mock_last_check.return_value = "Specific"
|
|
result = code_funcs.extract_codes_from_service(
|
|
{"SERVICE_TERM": "TEST SERVICE"}, self.constants
|
|
)
|
|
self.assertEqual(result["CODE_METHODOLOGY"], "Specific - No Match")
|
|
|
|
# Test no match - generic
|
|
mock_last_check.return_value = "Generic"
|
|
result = code_funcs.extract_codes_from_service(
|
|
{"SERVICE_TERM": "TEST SERVICE"}, self.constants
|
|
)
|
|
self.assertEqual(result["CODE_METHODOLOGY"], "Generic - No Match")
|
|
|
|
def test_code_in_range(self):
|
|
"""Test _code_in_range for numeric and alphanumeric range keys."""
|
|
self.assertTrue(code_funcs._code_in_range("00100-01999", "01234"))
|
|
self.assertTrue(code_funcs._code_in_range("00100-01999", "00100"))
|
|
self.assertTrue(code_funcs._code_in_range("00100-01999", "01999"))
|
|
self.assertFalse(code_funcs._code_in_range("00100-01999", "99999"))
|
|
self.assertFalse(code_funcs._code_in_range("00100-01999", "02000"))
|
|
self.assertTrue(code_funcs._code_in_range("A0021-A0999", "A0500"))
|
|
self.assertFalse(code_funcs._code_in_range("A0021-A0999", "B0001"))
|
|
self.assertFalse(code_funcs._code_in_range("", "12345"))
|
|
self.assertFalse(code_funcs._code_in_range("00100-01999", ""))
|
|
|
|
def test_lookup_procedure_description(self):
|
|
"""Test _lookup_procedure_description: exact match and range match."""
|
|
desc = code_funcs._lookup_procedure_description("12345", self.constants)
|
|
self.assertEqual(desc, "Test Procedure")
|
|
desc = code_funcs._lookup_procedure_description("99213", self.constants)
|
|
self.assertEqual(desc, "Office visit, established patient")
|
|
desc = code_funcs._lookup_procedure_description(
|
|
"NOT_ESTABLISHED", self.constants
|
|
)
|
|
self.assertEqual(desc, "")
|
|
desc = code_funcs._lookup_procedure_description("UNMAPPED99", self.constants)
|
|
self.assertEqual(desc, "")
|
|
|
|
def test_lookup_revenue_description(self):
|
|
"""Test _lookup_revenue_description."""
|
|
desc = code_funcs._lookup_revenue_description("0456", self.constants)
|
|
self.assertEqual(desc, "Advanced Revenue")
|
|
desc = code_funcs._lookup_revenue_description("9999", self.constants)
|
|
self.assertEqual(desc, "")
|
|
|
|
def test_fill_code_descriptions_from_mappings(self):
|
|
"""Test fill_code_descriptions_from_mappings fills _DESC from mappings."""
|
|
answer_dict = {
|
|
"PROCEDURE_CD": ["12345", "99213"],
|
|
"REVENUE_CD": ["0456"],
|
|
}
|
|
code_funcs.fill_code_descriptions_from_mappings(answer_dict, self.constants)
|
|
self.assertEqual(
|
|
answer_dict["PROCEDURE_CD_DESC"],
|
|
["Test Procedure", "Office visit, established patient"],
|
|
)
|
|
self.assertEqual(answer_dict["REVENUE_CD_DESC"], ["Advanced Revenue"])
|
|
|
|
def test_fill_code_descriptions_leave_unmapped_unchanged(self):
|
|
"""Test fill leaves _DESC empty for unmapped codes and NOT_ESTABLISHED."""
|
|
answer_dict = {
|
|
"PROCEDURE_CD": ["12345", "NOT_ESTABLISHED", "UNMAPPED99"],
|
|
"PROCEDURE_CD_DESC": ["", "", ""],
|
|
}
|
|
code_funcs.fill_code_descriptions_from_mappings(answer_dict, self.constants)
|
|
self.assertEqual(answer_dict["PROCEDURE_CD_DESC"][0], "Test Procedure")
|
|
self.assertEqual(answer_dict["PROCEDURE_CD_DESC"][1], "")
|
|
self.assertEqual(answer_dict["PROCEDURE_CD_DESC"][2], "")
|
|
|
|
def test_validate_explicit_codes_keeps_valid_filters_invalid(self):
|
|
"""Format-invalid codes dropped; format-valid preserved; has_unmatched if not in mapping."""
|
|
code_answer_dict = {
|
|
"PROCEDURE_CD": ["12345", "99213", "garbage7"],
|
|
"REVENUE_CD": ["0456", "9999"],
|
|
}
|
|
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
|
code_answer_dict, self.constants
|
|
)
|
|
self.assertTrue(has_unmatched)
|
|
self.assertEqual(filtered["PROCEDURE_CD"], ["12345", "99213"])
|
|
self.assertEqual(filtered["REVENUE_CD"], ["0456", "9999"])
|
|
|
|
def test_validate_explicit_codes_keeps_category_and_not_established(self):
|
|
"""Test validate_explicit_codes keeps Category: and NOT_ESTABLISHED; no unmatched."""
|
|
code_answer_dict = {"PROCEDURE_CD": ["Category: J", "NOT_ESTABLISHED"]}
|
|
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
|
code_answer_dict, self.constants
|
|
)
|
|
self.assertFalse(has_unmatched)
|
|
self.assertEqual(filtered["PROCEDURE_CD"], ["Category: J", "NOT_ESTABLISHED"])
|
|
|
|
def test_validate_explicit_codes_all_valid(self):
|
|
"""Test validate_explicit_codes returns has_unmatched False when all in mapping."""
|
|
code_answer_dict = {"PROCEDURE_CD": ["12345"], "REVENUE_CD": ["0456"]}
|
|
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
|
code_answer_dict, self.constants
|
|
)
|
|
self.assertFalse(has_unmatched)
|
|
self.assertEqual(filtered["PROCEDURE_CD"], ["12345"])
|
|
self.assertEqual(filtered["REVENUE_CD"], ["0456"])
|
|
|
|
def test_validate_explicit_codes_multiple_and_range(self):
|
|
"""Range key that is an explicit key in mapping is kept and counts as mapped."""
|
|
self.constants.CPT_LEVEL1_MAPPING["00100-01999"] = "Anesthesia"
|
|
code_answer_dict = {
|
|
"PROCEDURE_CD": ["12345", "99213", "00100-01999"],
|
|
"REVENUE_CD": ["0456"],
|
|
}
|
|
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
|
code_answer_dict, self.constants
|
|
)
|
|
self.assertFalse(has_unmatched)
|
|
self.assertEqual(filtered["PROCEDURE_CD"], ["12345", "99213", "00100-01999"])
|
|
self.assertEqual(filtered["REVENUE_CD"], ["0456"])
|
|
|
|
def test_validate_explicit_codes_range_not_in_mapping_kept_tagged_unmatched(self):
|
|
"""Format-valid range not in mapping is kept; has_unmatched True for tag."""
|
|
# Mock has 99213 in CPT_FULL_MAPPING but not 10021-69990; range is kept and tagged
|
|
code_answer_dict = {
|
|
"PROCEDURE_CD": ["99213", "10021-69990"],
|
|
"REVENUE_CD": ["0456"],
|
|
}
|
|
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
|
code_answer_dict, self.constants
|
|
)
|
|
self.assertTrue(has_unmatched)
|
|
self.assertEqual(filtered["PROCEDURE_CD"], ["99213", "10021-69990"])
|
|
self.assertEqual(filtered["REVENUE_CD"], ["0456"])
|
|
|
|
def test_validate_explicit_codes_single_code_unmapped(self):
|
|
"""Format-valid code not in exact mapping is preserved and has_unmatched True."""
|
|
code_answer_dict = {"PROCEDURE_CD": ["01234"], "REVENUE_CD": []}
|
|
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
|
code_answer_dict, self.constants
|
|
)
|
|
self.assertTrue(has_unmatched)
|
|
self.assertEqual(filtered["PROCEDURE_CD"], ["01234"])
|
|
|
|
def test_validate_explicit_codes_rejects_icd10_pcs_in_procedure(self):
|
|
"""ICD-10-PCS (7-char) filtered from PROCEDURE_CD; CPT/HCPCS format kept; S2900 in mapping."""
|
|
self.constants.HCPCS_FULL_MAPPING["S2900"] = "Robotic surgical system"
|
|
code_answer_dict = {
|
|
"PROCEDURE_CD": ["0E0V0CZ", "0E0W0CZ", "S2900"],
|
|
"REVENUE_CD": [],
|
|
}
|
|
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
|
code_answer_dict, self.constants
|
|
)
|
|
self.assertFalse(has_unmatched)
|
|
self.assertEqual(filtered["PROCEDURE_CD"], ["S2900"])
|
|
|
|
def test_has_unmatched_codes_true_for_unmapped_procedure(self):
|
|
"""_has_unmatched_codes returns True when procedure code is format-valid but not in mapping."""
|
|
d = {"PROCEDURE_CD": ["99999"], "REVENUE_CD": []}
|
|
self.assertTrue(code_funcs._has_unmatched_codes(d, self.constants))
|
|
|
|
def test_has_unmatched_codes_true_for_unmapped_revenue(self):
|
|
"""_has_unmatched_codes returns True when revenue code is format-valid but not in mapping."""
|
|
d = {"PROCEDURE_CD": [], "REVENUE_CD": ["9999"]}
|
|
self.assertTrue(code_funcs._has_unmatched_codes(d, self.constants))
|
|
|
|
def test_has_unmatched_codes_false_when_all_mapped(self):
|
|
"""_has_unmatched_codes returns False when all procedure/revenue codes are in mapping."""
|
|
d = {"PROCEDURE_CD": ["99213"], "REVENUE_CD": ["0456"]}
|
|
self.assertFalse(code_funcs._has_unmatched_codes(d, self.constants))
|
|
|
|
@patch("src.codes.code_funcs.code_explicit")
|
|
def test_extract_codes_explicit_unmatched(self, mock_explicit):
|
|
"""Explicit path sets Unmatched when any format-valid code not in mapping; no retry."""
|
|
mock_explicit.return_value = {
|
|
"PROCEDURE_CD": ["12345", "99999"],
|
|
"REVENUE_CD": [],
|
|
}
|
|
with patch("src.codes.code_funcs.clean_service", return_value="CLEAN"):
|
|
with patch("src.codes.code_funcs.fill_bill_type", return_value={}):
|
|
with patch(
|
|
"src.codes.code_funcs.get_implicit_runs",
|
|
return_value={"PROCEDURE_CD": True, "REVENUE_CD": True},
|
|
):
|
|
result = code_funcs.extract_codes_from_service(
|
|
{"SERVICE_TERM": "TEST"}, self.constants
|
|
)
|
|
self.assertEqual(result["CODE_METHODOLOGY"], "Explicit - Unmatched")
|
|
self.assertIn("12345", str(result["PROCEDURE_CD"]))
|
|
self.assertIn("99999", str(result["PROCEDURE_CD"]))
|
|
self.assertEqual(mock_explicit.call_count, 1)
|
|
|
|
@patch("src.codes.code_funcs.code_explicit")
|
|
def test_extract_codes_explicit_description_filled(self, mock_explicit):
|
|
"""Test explicit path fills PROCEDURE_CD_DESC from mapping."""
|
|
mock_explicit.return_value = {
|
|
"PROCEDURE_CD": ["99213"],
|
|
"REVENUE_CD": ["0456"],
|
|
}
|
|
with patch("src.codes.code_funcs.clean_service", return_value="CLEAN"):
|
|
with patch("src.codes.code_funcs.fill_bill_type", return_value={}):
|
|
with patch(
|
|
"src.codes.code_funcs.get_implicit_runs",
|
|
return_value={"PROCEDURE_CD": True, "REVENUE_CD": True},
|
|
):
|
|
result = code_funcs.extract_codes_from_service(
|
|
{"SERVICE_TERM": "Office visit"}, self.constants
|
|
)
|
|
self.assertEqual(result["CODE_METHODOLOGY"], "Explicit")
|
|
self.assertIn("99213", str(result["PROCEDURE_CD"]))
|
|
self.assertIn("Office visit", str(result["PROCEDURE_CD_DESC"]))
|
|
self.assertIn("Advanced Revenue", str(result["REVENUE_CD_DESC"]))
|
|
|
|
@patch("src.codes.code_funcs.code_implicit_arbitration")
|
|
@patch("src.codes.code_funcs.build_implicit_candidates")
|
|
@patch("src.codes.code_funcs.code_explicit")
|
|
def test_extract_codes_implicit_arbitration_unmatched(
|
|
self, mock_explicit, mock_build_candidates, mock_arbitration
|
|
):
|
|
"""Implicit path appends ' - Unmatched' when chosen candidate has unmapped code."""
|
|
mock_explicit.return_value = {
|
|
"PROCEDURE_CD": ["Category: J"],
|
|
"REVENUE_CD": [],
|
|
}
|
|
mock_build_candidates.return_value = [
|
|
{
|
|
"source": "RAG",
|
|
"code_answer_dict": {
|
|
"PROCEDURE_CD": ["99999"],
|
|
"REVENUE_CD": [],
|
|
},
|
|
}
|
|
]
|
|
chosen = {
|
|
"PROCEDURE_CD": ["99999"],
|
|
"REVENUE_CD": [],
|
|
"CODE_METHODOLOGY": "Implicit - Arbitration (RAG)",
|
|
}
|
|
mock_arbitration.return_value = chosen
|
|
with patch("src.codes.code_funcs.clean_service", return_value="CLEAN"):
|
|
with patch("src.codes.code_funcs.fill_bill_type", return_value={}):
|
|
with patch(
|
|
"src.codes.code_funcs.get_implicit_runs",
|
|
return_value={"PROCEDURE_CD": True, "REVENUE_CD": True},
|
|
):
|
|
result = code_funcs.extract_codes_from_service(
|
|
{"SERVICE_TERM": "J CODES"}, self.constants
|
|
)
|
|
self.assertEqual(
|
|
result["CODE_METHODOLOGY"], "Implicit - Arbitration (RAG) - Unmatched"
|
|
)
|
|
self.assertIn("99999", str(result["PROCEDURE_CD"]))
|
|
|
|
@patch("src.codes.code_funcs.code_implicit_arbitration")
|
|
@patch("src.codes.code_funcs.build_implicit_candidates")
|
|
@patch("src.codes.code_funcs.code_explicit")
|
|
def test_extract_codes_implicit_arbitration_all_mapped(
|
|
self, mock_explicit, mock_build_candidates, mock_arbitration
|
|
):
|
|
"""Implicit path does not append ' - Unmatched' when all codes are in mapping."""
|
|
mock_explicit.return_value = {
|
|
"PROCEDURE_CD": ["Category: J"],
|
|
"REVENUE_CD": [],
|
|
}
|
|
mock_build_candidates.return_value = [
|
|
{
|
|
"source": "RAG",
|
|
"code_answer_dict": {
|
|
"PROCEDURE_CD": ["99213"],
|
|
"REVENUE_CD": ["0456"],
|
|
},
|
|
}
|
|
]
|
|
chosen = {
|
|
"PROCEDURE_CD": ["99213"],
|
|
"REVENUE_CD": ["0456"],
|
|
"CODE_METHODOLOGY": "Implicit - Arbitration (RAG)",
|
|
}
|
|
mock_arbitration.return_value = chosen
|
|
with patch("src.codes.code_funcs.clean_service", return_value="CLEAN"):
|
|
with patch("src.codes.code_funcs.fill_bill_type", return_value={}):
|
|
with patch(
|
|
"src.codes.code_funcs.get_implicit_runs",
|
|
return_value={"PROCEDURE_CD": True, "REVENUE_CD": True},
|
|
):
|
|
result = code_funcs.extract_codes_from_service(
|
|
{"SERVICE_TERM": "J CODES"}, self.constants
|
|
)
|
|
self.assertEqual(result["CODE_METHODOLOGY"], "Implicit - Arbitration (RAG)")
|
|
self.assertIn("99213", str(result["PROCEDURE_CD"]))
|
|
self.assertIn("0456", str(result["REVENUE_CD"]))
|
|
|
|
@patch("src.codes.code_funcs.extract_codes_from_service")
|
|
def test_code_breakout(self, mock_extract_codes):
|
|
"""Test the code_breakout function for processing DataFrame records."""
|
|
# BILL_TYPE_CD_DESC set so pre-pass skips fill_bill_type (avoids LLM on CI).
|
|
test_df = pd.DataFrame(
|
|
[
|
|
{
|
|
"SERVICE_TERM": "SERVICE A",
|
|
"BILL_TYPE_CD_DESC": "Inpatient Hospital",
|
|
},
|
|
{
|
|
"SERVICE_TERM": "SERVICE B",
|
|
"BILL_TYPE_CD_DESC": "Inpatient Hospital",
|
|
},
|
|
]
|
|
)
|
|
|
|
# Return value by SERVICE_TERM so order is correct under ThreadPoolExecutor.
|
|
def return_by_service(rep, constants):
|
|
st = rep.get("SERVICE_TERM", "")
|
|
if st == "SERVICE A":
|
|
return {"SERVICE_TERM": "SERVICE A", "PROCEDURE_CD": ["12345"]}
|
|
return {"SERVICE_TERM": "SERVICE B", "PROCEDURE_CD": ["67890"]}
|
|
|
|
mock_extract_codes.side_effect = return_by_service
|
|
|
|
# Test function
|
|
result_df = code_funcs.code_breakout(test_df, self.constants)
|
|
|
|
# Verify results - code_breakout normalizes to JSON list format
|
|
self.assertIsInstance(result_df, pd.DataFrame)
|
|
self.assertEqual(len(result_df), 2)
|
|
self.assertEqual(result_df.iloc[0]["PROCEDURE_CD"], '["12345"]')
|
|
self.assertEqual(result_df.iloc[1]["PROCEDURE_CD"], '["67890"]')
|
|
|
|
# Verify mock calls
|
|
self.assertEqual(mock_extract_codes.call_count, 2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|