ca53ff8395
Bugfix/cpt codes * formatting fix for revenue_cd * format fix for procedure_codes * bugfix for revenue_codes * working version from code main * code formatting from main and code main * removed dubug prints * removed local file paths * standardize state fields * updated test scenerios * minor fix * Merge branch 'main' into bugfix/cpt-codes merging main into cpt code fix * checks for all fields * reused is_empty functionality * moved normalize_to_json_list to utils * Merge branch 'main' into bugfix/cpt-codes merging upto date main * minor touches * pytest fix Approved-by: Katon Minhas
453 lines
18 KiB
Python
453 lines
18 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
|
|
from constants.constants import Constants
|
|
from constants.delimiters import Delimiter
|
|
|
|
|
|
class TestCodeFuncs(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures before each test method."""
|
|
# Create a mock Constants object
|
|
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 = {"456": "Advanced Revenue"}
|
|
|
|
self.constants.BILL_TYPE_MAPPING = {"11X": "Inpatient Hospital"}
|
|
self.constants.BILL_TYPE_REVERSE_MAPPING = {"Inpatient Hospital": "11X"}
|
|
|
|
# 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 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")
|
|
@patch("src.utils.string_utils.extract_text_from_delimiters")
|
|
def test_code_implicit_special(self, mock_extract, mock_invoke_claude):
|
|
"""Test the code_implicit_special function for special case handling."""
|
|
# Setup mocks
|
|
mock_invoke_claude.return_value = "mock_response"
|
|
mock_extract.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
|
|
mock_extract.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 lists
|
|
self.assertEqual(result["PROCEDURE_CD"], ["12345"])
|
|
self.assertEqual(result["PROCEDURE_CD_DESC"], ["Test Procedure"])
|
|
self.assertEqual(result["CODE_METHODOLOGY"], "Implicit - Level 1")
|
|
|
|
# Test exception handling
|
|
mock_json_load.side_effect = Exception("Test error")
|
|
result = code_funcs.code_implicit_rag(
|
|
"TEST SERVICE", implicit_run_dict, "test.pdf", self.constants
|
|
)
|
|
self.assertIn("CODE_METHODOLOGY", result)
|
|
|
|
@patch("src.utils.llm_utils.invoke_claude")
|
|
@patch("src.utils.string_utils.extract_text_from_delimiters")
|
|
def test_code_last_check(self, mock_extract, mock_invoke_claude):
|
|
"""Test the code_last_check function for categorizing non-matched services."""
|
|
# Setup mocks
|
|
mock_invoke_claude.return_value = "mock_response"
|
|
|
|
# Test specific case
|
|
mock_extract.return_value = "Specific"
|
|
result = code_funcs.code_last_check("SPECIAL SERVICE", "test.pdf")
|
|
self.assertEqual(result, "Specific")
|
|
|
|
# Test generic case
|
|
mock_extract.return_value = "Generic"
|
|
result = code_funcs.code_last_check("GENERIC SERVICE", "test.pdf")
|
|
self.assertEqual(result, "Generic")
|
|
|
|
@patch("src.utils.llm_utils.invoke_claude")
|
|
@patch("src.utils.string_utils.universal_json_load")
|
|
def test_fill_bill_type(self, mock_json_load, mock_invoke_claude):
|
|
"""Test the fill_bill_type function for populating bill type information."""
|
|
# Setup mocks
|
|
mock_invoke_claude.return_value = '["Inpatient Hospital"]'
|
|
mock_json_load.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,
|
|
)
|
|
|
|
self.assertEqual(result["BILL_TYPE_CD"], "11X")
|
|
self.assertEqual(result["BILL_TYPE_CD_DESC"], "Inpatient Hospital")
|
|
|
|
# Test with empty response
|
|
mock_json_load.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
|
|
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 - Letter Category")
|
|
|
|
# Test special case match
|
|
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
|
|
)
|
|
self.assertEqual(result["CODE_METHODOLOGY"], "Implicit - Special Case")
|
|
|
|
# Test RAG match
|
|
mock_implicit_special.return_value = {}
|
|
mock_implicit_rag.return_value = {
|
|
"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 - 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_fill_claim_type(self):
|
|
"""Test the fill_claim_type function for determining claim types."""
|
|
# Test with existing claim types
|
|
answer_dicts = [
|
|
{"AARETE_DERIVED_CLAIM_TYPE_CD": "H"},
|
|
{"AARETE_DERIVED_CLAIM_TYPE_CD": "H"},
|
|
{"AARETE_DERIVED_CLAIM_TYPE_CD": "M"},
|
|
{},
|
|
]
|
|
result = code_funcs.fill_claim_type(answer_dicts)
|
|
self.assertEqual(result[3]["AARETE_DERIVED_CLAIM_TYPE_CD"], "H")
|
|
|
|
# Test with all empty claim types
|
|
answer_dicts = [{}, {}]
|
|
result = code_funcs.fill_claim_type(answer_dicts)
|
|
self.assertEqual(result, answer_dicts)
|
|
|
|
@patch("src.codes.code_funcs.fill_claim_type")
|
|
@patch("src.codes.code_funcs.extract_codes_from_service")
|
|
def test_code_breakout(self, mock_extract_codes, mock_fill_claim_type):
|
|
"""Test the code_breakout function for processing DataFrame records."""
|
|
# Create test data
|
|
test_df = pd.DataFrame(
|
|
[{"SERVICE_TERM": "SERVICE A"}, {"SERVICE_TERM": "SERVICE B"}]
|
|
)
|
|
|
|
# Setup mocks
|
|
mock_fill_claim_type.return_value = [
|
|
{"SERVICE_TERM": "SERVICE A"},
|
|
{"SERVICE_TERM": "SERVICE B"},
|
|
]
|
|
|
|
mock_extract_codes.side_effect = [
|
|
{"SERVICE_TERM": "SERVICE A", "PROCEDURE_CD": "12345"},
|
|
{"SERVICE_TERM": "SERVICE B", "PROCEDURE_CD": "67890"},
|
|
]
|
|
|
|
# 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
|
|
mock_fill_claim_type.assert_called_once()
|
|
self.assertEqual(mock_extract_codes.call_count, 2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|