Files
doczyai-pipelines/fieldExtraction/tests/test_one_to_n.py
T
Katon Minhas 0e2622f156 Merged in feature/1toN-Optimization (pull request #799)
Feature/1toN Optimization

* Update config - remove Haiku 3

* Merged in optimization/trigger-cap (pull request #792)

Optimization/trigger cap

* trigger cap breakout added

* Merge remote-tracking branch 'origin/feature/1toN-Optimization' into optimization/trigger-cap

* carveout breakout added


Approved-by: Katon Minhas

* Merged in bugfix/utah_issue_fixes (pull request #791)

Bugfix/utah issue fixes to feature/1toN-Optimization

* updated valid values for AARETE_DERIVED_REIMB_METHOD

* removed example reimbursements

* removed reciprocatory agreements section reimbs

* Merged feature/1toN-Optimization into bugfix/utah_issue_fixes

* update reimb primary


Approved-by: Katon Minhas

* Update unit tests

* Update llm_utils

* Merge branch 'feature/deprecate-haiku-3' into feature/1toN-Optimization

* fix over-filtering of lesser of

* Merged in bugfix/UT-methodology-breakout (pull request #794)

Bugfix/UT methodology breakout

* updated valid values for AARETE_DERIVED_REIMB_METHOD

* removed example reimbursements

* prompt update

* prompt update

* Merged feature/1toN-Optimization into bugfix/nv_issue_fixes

* add service term in mb prompts

* Merged feature/1toN-Optimization into bugfix/UT-methodology-breakout

* print statement removed

* Merge branch 'bugfix/UT-methodology-breakout' of https://bitbucket.org/aarete/doczy.ai into bugfix/UT-methodology-breakout

* primary prompt update

* remove duplicate prompt


Approved-by: Katon Minhas

* Merge branch 'main' into feature/1toN-Optimization

* Merge branch 'main' into feature/1toN-Optimization

* Update preprocessing to make the exhibit_chunk_mapping start at first page

* Merge row_funcs.py changes from feature/cross-exhibit-dynamic

* Merged in bugfix/UT-grouper-issues (pull request #796)

Bugfix/UT grouper issues

* updated valid values for AARETE_DERIVED_REIMB_METHOD

* removed example reimbursements

* prompt update

* prompt update

* Merged feature/1toN-Optimization into bugfix/nv_issue_fixes

* add service term in mb prompts

* Merged feature/1toN-Optimization into bugfix/UT-methodology-breakout

* print statement removed

* Merge branch 'bugfix/UT-methodology-breakout' of https://bitbucket.org/aarete/doczy.ai into bugfix/UT-methodology-breakout

* primary prompt update

* prompt update

* Merge remote-tracking branch 'origin/feature/1toN-Optimization' into bugfix/UT-grouper-issues

* removed temp changes

* removed temp changes

* Update reimb primary


Approved-by: Katon Minhas

* Merged in bugfix/validation_fixes (pull request #795)

bugfix/validation_fixes to feature/1toN-Optimization

* updated validation of clean claims reimbursement

* Merged feature/1toN-Optimization into bugfix/validation_fixes


Approved-by: Katon Minhas

* Re-add dynamic codes and reimb-info

* Re-structure empty reimbursement prompt

* Fix lesser of check overfiltering

* Merged in bugfix/generic-methodology-breakout (pull request #797)

Bugfix/generic methodology breakout

* fee schedule fix

* carveout+special case prompt changed

* fee schedule changes

* prompt changes and column order


Approved-by: Katon Minhas

* pytest fix

* remove prints

* Update mapping

* Update tests

* Update test

* Address PR review comments: add logging, fix return types, fix variable propagation, and add tests

- Add warning log in code_funcs.py try/except block for grouper code parsing failures
- Add warning log in hybrid_smart_chunking_funcs.py except block for JSON parsing failures
- Add return type annotation to lesser_of_distribution function
- Fix first_reimbursement_page variable propagation issue by returning updated value
- Add comprehensive tests for simplify_exhibit, chunk_by_exhibit, and lesser_of_distribution functions

* Merged in bugfix/prov_info_json (pull request #802)

Bugfix/prov info json to feature/1toN-Optimization

* prov_info_json added

* remove print statements


Approved-by: Katon Minhas


Approved-by: Siddhant Medar
2025-12-10 20:01:52 +00:00

698 lines
31 KiB
Python

import unittest
from unittest.mock import Mock, patch, MagicMock
from src.investment import one_to_n_funcs
from src.prompts.fieldset import FieldSet
from constants.constants import Constants
class TestOneToNFuncs(unittest.TestCase):
"""Test suite for one_to_n_funcs.py functions."""
def setUp(self):
"""Set up test fixtures."""
self.constants = Constants()
self.filename = "test_contract.pdf"
self.exhibit_text = "This is a sample exhibit text with reimbursement terms."
self.exhibit_header = "Exhibit A - Fee Schedule"
self.exhibit_page = "1"
# ==================== exhibit_level Tests ====================
@patch('src.utils.llm_utils.invoke_claude') # Mock at the invoke_claude level
@patch('src.investment.one_to_n_funcs.dynamic_funcs.dynamic_primary')
def test_exhibit_level_basic(self, mock_dynamic_primary, mock_invoke_claude):
"""Test basic exhibit level processing."""
# Mock LLM response for dynamic_primary call
mock_invoke_claude.return_value = '{"AARETE_DERIVED_LOB": "Medicaid", "AARETE_DERIVED_NETWORK": "In-Network"}'
# Mock dynamic primary returning updated answers and fields
mock_dynamic_fields = Mock(spec=FieldSet)
mock_dynamic_primary.return_value = (
{"SERVICE_CATEGORY_CD": "INPATIENT", "PROV_TYPE_CD": "HOSPITAL", "AARETE_DERIVED_LOB": "Medicaid"},
mock_dynamic_fields
)
exhibit_level_answers, dynamic_fields = one_to_n_funcs.exhibit_level(
self.exhibit_text, self.exhibit_header, self.exhibit_page, self.constants, self.filename
)
# Assertions
assert exhibit_level_answers is not None
mock_invoke_claude.assert_called()
# ==================== reimbursement_level Tests ====================
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_reimbursement_primary')
@patch('src.investment.one_to_n_funcs.clean_reimbursement_primary')
def test_reimbursement_level_with_results(self, mock_clean, mock_prompt_reimb):
"""Test reimbursement level extraction with valid results."""
mock_prompt_reimb.return_value = [
{"SERVICE_TERM": "Lab Services", "REIMB_TERM": "100% of Medicare"},
{"SERVICE_TERM": "Radiology", "REIMB_TERM": "90% of billed charges"}
]
mock_clean.return_value = mock_prompt_reimb.return_value
page_text = "Lab Services will be reimbursed at 100% of Medicare."
result = one_to_n_funcs.reimbursement_level(page_text, self.constants, self.filename)
self.assertEqual(len(result), 2)
mock_prompt_reimb.assert_called_once_with(page_text, self.filename)
mock_clean.assert_called_once()
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_reimbursement_primary')
def test_reimbursement_level_no_results(self, mock_prompt_reimb):
"""Test reimbursement level when no reimbursements found."""
mock_prompt_reimb.return_value = None
page_text = "This page has no reimbursement information."
result = one_to_n_funcs.reimbursement_level(page_text, self.constants, self.filename)
self.assertIsNone(result)
# ==================== clean_reimbursement_primary Tests ====================
@patch('src.investment.one_to_n_funcs.filter_services_without_reimbursements')
def test_clean_reimbursement_primary_basic(self, mock_filter):
"""Test basic cleaning of reimbursement primary answers."""
input_answers = [
{"SERVICE_TERM": "Labs", "REIMB_TERM": "100% of Medicare"},
{"SERVICE_TERM": "X-Ray", "REIMB_TERM": "Fee schedule"}
]
mock_filter.return_value = input_answers
result = one_to_n_funcs.clean_reimbursement_primary(
input_answers, self.constants, self.filename
)
self.assertEqual(len(result), 2)
mock_filter.assert_called_once_with(input_answers, self.filename)
def test_clean_reimbursement_primary_empty_input(self):
"""Test cleaning with empty input."""
result = one_to_n_funcs.clean_reimbursement_primary(
[], self.constants, self.filename
)
self.assertEqual(result, [])
def test_clean_reimbursement_primary_none_input(self):
"""Test cleaning with None input."""
result = one_to_n_funcs.clean_reimbursement_primary(
None, self.constants, self.filename
)
self.assertEqual(result, [])
# ==================== methodology_breakout Tests ====================
@patch('src.investment.one_to_n_funcs.methodology_breakout_single_row')
def test_methodology_breakout_multiple_rows(self, mock_single_row):
"""Test methodology breakout with multiple rows."""
mock_single_row.side_effect = [
[{"SERVICE_TERM": "Labs", "AARETE_DERIVED_REIMB_METHOD": "Fee Schedule"}],
[{"SERVICE_TERM": "Surgery", "AARETE_DERIVED_REIMB_METHOD": "Grouper"}]
]
input_answers = [
{"SERVICE_TERM": "Labs", "REIMB_TERM": "Medicare fee schedule"},
{"SERVICE_TERM": "Surgery", "REIMB_TERM": "MS-DRG"}
]
result = one_to_n_funcs.methodology_breakout(
input_answers, self.constants, self.filename
)
self.assertEqual(len(result), 2)
self.assertEqual(mock_single_row.call_count, 2)
# ==================== methodology_breakout_single_row Tests ====================
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_methodology_breakout')
@patch('src.investment.one_to_n_funcs.methodology_breakout_secondary')
def test_methodology_breakout_single_row_basic(self, mock_secondary, mock_prompt):
"""Test single row methodology breakout."""
mock_prompt.return_value = {
"AARETE_DERIVED_REIMB_METHOD": "Fee Schedule",
"REIMB_FEE_RATE": "N/A",
"REIMB_PCT_RATE": "100"
}
mock_secondary.return_value = {"FEE_SCHEDULE": "Medicare"}
answer_dict = {"SERVICE_TERM": "Labs", "REIMB_TERM": "100% of Medicare"}
result = one_to_n_funcs.methodology_breakout_single_row(
answer_dict, self.constants, self.filename
)
self.assertEqual(len(result), 1)
self.assertIn("AARETE_DERIVED_REIMB_METHOD", result[0])
self.assertIn("FEE_SCHEDULE", result[0])
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_methodology_breakout')
def test_methodology_breakout_single_row_no_reimb_term(self, mock_prompt):
"""Test single row breakout when REIMB_TERM is missing."""
answer_dict = {"SERVICE_TERM": "Labs"}
result = one_to_n_funcs.methodology_breakout_single_row(
answer_dict, self.constants, self.filename
)
self.assertEqual(result, [answer_dict])
mock_prompt.assert_not_called()
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_methodology_breakout')
@patch('src.investment.one_to_n_funcs.methodology_breakout_secondary')
def test_methodology_breakout_single_row_list_output(self, mock_secondary, mock_prompt):
"""Test single row breakout when LLM returns list of methodologies."""
mock_prompt.return_value = [
{"AARETE_DERIVED_REIMB_METHOD": "Fee Schedule"},
{"AARETE_DERIVED_REIMB_METHOD": "Per Diem"}
]
mock_secondary.side_effect = [
{"FEE_SCHEDULE": "Medicare"},
{"FEE_SCHEDULE": "N/A"}
]
answer_dict = {"SERVICE_TERM": "Labs", "REIMB_TERM": "Medicare or $500 per day"}
result = one_to_n_funcs.methodology_breakout_single_row(
answer_dict, self.constants, self.filename
)
self.assertEqual(len(result), 2)
self.assertEqual(mock_secondary.call_count, 2)
# ==================== methodology_breakout_secondary Tests ====================
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_fee_schedule_breakout')
def test_methodology_breakout_secondary_fee_schedule(self, mock_fs_breakout):
"""Test secondary breakout for Fee Schedule methodology."""
mock_fs_breakout.return_value = {
"FEE_SCHEDULE": "Medicare",
"FEE_SCHEDULE_VERSION": "2024"
}
methodology_dict = {"AARETE_DERIVED_REIMB_METHOD": "Fee Schedule"}
with patch('src.investment.one_to_n_funcs.FieldSet') as mock_fieldset:
mock_grouper_fields = Mock()
mock_grouper_fields.list_fields.return_value = ["GROUPER_TYPE", "DRG_WEIGHT_TYPE"]
mock_fieldset.return_value = mock_grouper_fields
result = one_to_n_funcs.methodology_breakout_secondary(
methodology_dict, "Labs", "Medicare fee schedule",
Mock(), mock_grouper_fields, self.constants, self.filename
)
self.assertIn("FEE_SCHEDULE", result)
self.assertEqual(result["GROUPER_TYPE"], "N/A")
self.assertEqual(result["DRG_WEIGHT_TYPE"], "N/A")
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_grouper_breakout')
def test_methodology_breakout_secondary_grouper(self, mock_grouper_breakout):
"""Test secondary breakout for Grouper methodology."""
mock_grouper_breakout.return_value = {
"GROUPER_TYPE": "MS-DRG",
"DRG_WEIGHT_TYPE": "CMS"
}
methodology_dict = {"AARETE_DERIVED_REIMB_METHOD": "Grouper"}
with patch('src.investment.one_to_n_funcs.FieldSet') as mock_fieldset:
mock_fs_fields = Mock()
mock_fs_fields.list_fields.return_value = ["FEE_SCHEDULE", "FEE_SCHEDULE_VERSION"]
mock_fieldset.return_value = mock_fs_fields
result = one_to_n_funcs.methodology_breakout_secondary(
methodology_dict, "Surgery", "MS-DRG",
mock_fs_fields, Mock(), self.constants, self.filename
)
self.assertIn("GROUPER_TYPE", result)
self.assertEqual(result["FEE_SCHEDULE"], "N/A")
self.assertEqual(result["FEE_SCHEDULE_VERSION"], "N/A")
def test_methodology_breakout_secondary_other_methodology(self):
"""Test secondary breakout for non-FS, non-Grouper methodology."""
methodology_dict = {"AARETE_DERIVED_REIMB_METHOD": "Per Diem"}
with patch('src.investment.one_to_n_funcs.FieldSet') as mock_fieldset:
mock_fields = Mock()
mock_fields.list_fields.return_value = ["FEE_SCHEDULE", "GROUPER_TYPE"]
mock_fieldset.return_value = mock_fields
result = one_to_n_funcs.methodology_breakout_secondary(
methodology_dict, "Inpatient", "$500 per day",
mock_fields, mock_fields, self.constants, self.filename
)
self.assertEqual(result["FEE_SCHEDULE"], "N/A")
self.assertEqual(result["GROUPER_TYPE"], "N/A")
# ==================== carveout_and_special_case Tests ====================
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_carveout_check')
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_special_case_breakout')
def test_carveout_and_special_case_base_covered(self, mock_special_breakout, mock_carveout_check):
"""Test carveout check for BASE_COVERED_SERVICES."""
mock_carveout_check.return_value = "BASE_COVERED_SERVICES"
input_answers = [{"SERVICE_TERM": "Labs", "REIMB_TERM": "100% of Medicare"}]
reimb_answers, special_answers = one_to_n_funcs.carveout_and_special_case(
input_answers, self.constants, self.filename
)
self.assertEqual(len(reimb_answers), 1)
self.assertEqual(len(special_answers), 0)
self.assertEqual(reimb_answers[0]["CARVEOUT_CD"], "BASE_COVERED_SERVICES")
self.assertEqual(reimb_answers[0]["CARVEOUT_IND"], "N")
self.assertEqual(reimb_answers[0]["DEFAULT_IND"], "N")
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_carveout_check')
def test_carveout_and_special_case_default_term(self, mock_carveout_check):
"""Test carveout check for DEFAULT_TERM."""
mock_carveout_check.return_value = "DEFAULT_TERM"
input_answers = [{"SERVICE_TERM": "All other services", "REIMB_TERM": "As negotiated"}]
reimb_answers, special_answers = one_to_n_funcs.carveout_and_special_case(
input_answers, self.constants, self.filename
)
self.assertEqual(reimb_answers[0]["CARVEOUT_CD"], "DEFAULT_TERM")
self.assertEqual(reimb_answers[0]["CARVEOUT_IND"], "N")
self.assertEqual(reimb_answers[0]["DEFAULT_IND"], "Y")
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_carveout_check')
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_special_case_breakout')
def test_carveout_and_special_case_trigger_cap(self, mock_special_breakout, mock_carveout_check):
"""Test carveout check for TRIGGER_CAP with breakout."""
mock_carveout_check.return_value = "TRIGGER_CAP"
mock_special_breakout.return_value = {"TRIGGER_AMOUNT": "$10000", "CAP_AMOUNT": "$50000"}
input_answers = [{"SERVICE_TERM": "Surgery", "REIMB_TERM": "Trigger at $10k, cap at $50k"}]
reimb_answers, special_answers = one_to_n_funcs.carveout_and_special_case(
input_answers, self.constants, self.filename
)
self.assertEqual(reimb_answers[0]["CARVEOUT_CD"], "TRIGGER_CAP")
self.assertEqual(reimb_answers[0]["CARVEOUT_IND"], "Y")
self.assertEqual(reimb_answers[0]["DEFAULT_IND"], "N")
self.assertIn("TRIGGER_AMOUNT", reimb_answers[0])
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_carveout_check')
def test_carveout_and_special_case_special_case_field(self, mock_carveout_check):
"""Test when result is a special case field, not a carveout."""
mock_carveout_check.return_value = "OUTLIER_TERM"
input_answers = [{"SERVICE_TERM": "High-cost cases", "REIMB_TERM": "Outlier payment above $25,000"}]
reimb_answers, special_answers = one_to_n_funcs.carveout_and_special_case(
input_answers, self.constants, self.filename
)
self.assertEqual(len(reimb_answers), 0)
self.assertEqual(len(special_answers), 1)
self.assertIn("OUTLIER_TERM", special_answers[0])
self.assertNotIn("SERVICE_TERM", special_answers[0])
self.assertNotIn("REIMB_TERM", special_answers[0])
# ==================== special_case_breakout Tests ====================
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_special_case_breakout')
def test_special_case_breakout_outlier(self, mock_breakout):
"""Test special case breakout for outlier terms."""
mock_breakout.return_value = {
"OUTLIER_THRESHOLD": "$25000",
"OUTLIER_PAYMENT_METHOD": "Cost-based"
}
input_answers = [{"OUTLIER_TERM": "Outlier payment for cases exceeding $25,000"}]
with patch('src.investment.one_to_n_funcs.FieldSet') as mock_fieldset:
mock_field = Mock()
mock_field.get_breakout_template.return_value = "OUTLIER_BREAKOUT template"
mock_fields = Mock()
mock_fields.get_field.return_value = mock_field
mock_fieldset.return_value = mock_fields
result = one_to_n_funcs.special_case_breakout(input_answers, self.filename)
self.assertEqual(len(result), 1)
self.assertIn("OUTLIER_THRESHOLD", result[0])
self.assertIn("OUTLIER_PAYMENT_METHOD", result[0])
# ==================== filter_services_without_reimbursements Tests ====================
@patch('src.investment.one_to_n_funcs.prompt_calls.validate_reimbursements_for_llm')
def test_filter_services_all_valid(self, mock_validate):
"""Test filtering when all services have valid reimbursements."""
mock_validate.return_value = True
input_answers = [
{"SERVICE_TERM": "Labs", "REIMB_TERM": "100% of Medicare"},
{"SERVICE_TERM": "X-Ray", "REIMB_TERM": "Fee schedule"}
]
result = one_to_n_funcs.filter_services_without_reimbursements(
input_answers, self.filename
)
self.assertEqual(len(result), 2)
self.assertEqual(mock_validate.call_count, 2)
@patch('src.investment.one_to_n_funcs.prompt_calls.validate_reimbursements_for_llm')
def test_filter_services_some_invalid(self, mock_validate):
"""Test filtering when some services lack reimbursements."""
mock_validate.side_effect = [True, False, True]
input_answers = [
{"SERVICE_TERM": "Labs", "REIMB_TERM": "100% of Medicare"},
{"SERVICE_TERM": "Definitions", "REIMB_TERM": "Standard eyeglass lenses as defined by DMAP"},
{"SERVICE_TERM": "Surgery", "REIMB_TERM": "MS-DRG"}
]
result = one_to_n_funcs.filter_services_without_reimbursements(
input_answers, self.filename
)
self.assertEqual(len(result), 2)
self.assertEqual(result[0]["SERVICE_TERM"], "Labs")
self.assertEqual(result[1]["SERVICE_TERM"], "Surgery")
@patch('src.investment.one_to_n_funcs.prompt_calls.validate_reimbursements_for_llm')
def test_filter_services_all_invalid(self, mock_validate):
"""Test filtering when all services are invalid."""
mock_validate.return_value = False
input_answers = [
{"SERVICE_TERM": "Admin", "REIMB_TERM": "Administrative terms apply"},
{"SERVICE_TERM": "Definitions", "REIMB_TERM": "See glossary"}
]
result = one_to_n_funcs.filter_services_without_reimbursements(
input_answers, self.filename
)
self.assertEqual(len(result), 0)
# ==================== split_reimb_dates Tests ====================
@patch('src.investment.one_to_n_funcs.llm_utils.invoke_claude')
@patch('src.investment.one_to_n_funcs.string_utils.universal_json_load')
def test_split_reimb_dates_valid_range(self, mock_json_load, mock_invoke):
"""Test splitting valid date range."""
mock_invoke.return_value = '{"start_date": "2024-01-01", "end_date": "2024-12-31"}'
mock_json_load.return_value = {"start_date": "2024-01-01", "end_date": "2024-12-31"}
input_data = [
{"SERVICE_TERM": "Labs", "REIMB_DATES": "January 1, 2024 through December 31, 2024"}
]
result = one_to_n_funcs.split_reimb_dates(input_data, self.filename)
self.assertEqual(result[0]["REIMB_EFFECTIVE_DT"], "2024-01-01")
self.assertEqual(result[0]["REIMB_TERMINATION_DT"], "2024-12-31")
self.assertNotIn("REIMB_DATES", result[0])
def test_split_reimb_dates_no_dates(self):
"""Test splitting when no REIMB_DATES field exists."""
input_data = [
{"SERVICE_TERM": "Labs", "REIMB_TERM": "100% of Medicare"}
]
result = one_to_n_funcs.split_reimb_dates(input_data, self.filename)
self.assertIsNone(result[0]["REIMB_EFFECTIVE_DT"])
self.assertIsNone(result[0]["REIMB_TERMINATION_DT"])
def test_split_reimb_dates_empty_dates(self):
"""Test splitting when REIMB_DATES is empty string."""
input_data = [
{"SERVICE_TERM": "Labs", "REIMB_DATES": ""}
]
result = one_to_n_funcs.split_reimb_dates(input_data, self.filename)
self.assertIsNone(result[0]["REIMB_EFFECTIVE_DT"])
self.assertIsNone(result[0]["REIMB_TERMINATION_DT"])
self.assertNotIn("REIMB_DATES", result[0])
def test_split_reimb_dates_empty_input(self):
"""Test splitting with empty input list."""
result = one_to_n_funcs.split_reimb_dates([], self.filename)
self.assertEqual(result, [])
def test_split_reimb_dates_none_input(self):
"""Test splitting with None input."""
result = one_to_n_funcs.split_reimb_dates(None, self.filename)
self.assertIsNone(result)
@patch('src.investment.one_to_n_funcs.llm_utils.invoke_claude')
def test_split_reimb_dates_llm_error(self, mock_invoke):
"""Test splitting when LLM call fails."""
mock_invoke.side_effect = Exception("LLM API error")
input_data = [
{"SERVICE_TERM": "Labs", "REIMB_DATES": "January 1, 2024 through December 31, 2024"}
]
result = one_to_n_funcs.split_reimb_dates(input_data, self.filename)
# Should still remove REIMB_DATES and initialize new fields
self.assertIsNone(result[0]["REIMB_EFFECTIVE_DT"])
self.assertIsNone(result[0]["REIMB_TERMINATION_DT"])
self.assertNotIn("REIMB_DATES", result[0])
# ==================== get_lob_relationship Tests ====================
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lob_relationship')
@patch('src.investment.one_to_n_funcs.string_utils.is_empty')
def test_get_lob_relationship_with_program(self, mock_is_empty, mock_prompt_lob):
"""Test LOB-Program relationship extraction."""
mock_is_empty.side_effect = [False, False, True] # LOB not empty, PROGRAM not empty, PRODUCT empty
mock_prompt_lob.return_value = "PROGRAM_SPECIFIC"
input_data = [
{"AARETE_DERIVED_LOB": "Medicaid", "AARETE_DERIVED_PROGRAM": "CHIP"}
]
result = one_to_n_funcs.get_lob_relationship(
input_data, self.exhibit_text, self.filename
)
self.assertEqual(result[0]["LOB_PROGRAM_RELATIONSHIP"], "PROGRAM_SPECIFIC")
mock_prompt_lob.assert_called_once()
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lob_relationship')
@patch('src.investment.one_to_n_funcs.string_utils.is_empty')
def test_get_lob_relationship_with_product(self, mock_is_empty, mock_prompt_lob):
"""Test LOB-Product relationship extraction."""
mock_is_empty.side_effect = [False, True, False] # LOB not empty, PROGRAM empty, PRODUCT not empty
mock_prompt_lob.return_value = "PRODUCT_SPECIFIC"
input_data = [
{"AARETE_DERIVED_LOB": "Commercial", "AARETE_DERIVED_PRODUCT": "HMO"}
]
result = one_to_n_funcs.get_lob_relationship(
input_data, self.exhibit_text, self.filename
)
self.assertEqual(result[0]["LOB_PRODUCT_RELATIONSHIP"], "PRODUCT_SPECIFIC")
mock_prompt_lob.assert_called_once()
@patch('src.investment.one_to_n_funcs.string_utils.is_empty')
def test_get_lob_relationship_no_lob(self, mock_is_empty):
"""Test relationship extraction when LOB is empty."""
mock_is_empty.return_value = True
input_data = [
{"AARETE_DERIVED_PROGRAM": "CHIP"}
]
result = one_to_n_funcs.get_lob_relationship(
input_data, self.exhibit_text, self.filename
)
# Should skip processing and return unchanged
self.assertEqual(result, input_data)
# ==================== one_to_n_cleaning Tests ====================
@patch('src.investment.one_to_n_funcs.aarete_derived.get_crosswalk_fields')
@patch('src.investment.one_to_n_funcs.get_lob_relationship')
@patch('src.investment.one_to_n_funcs.aarete_derived.fill_na_mapping')
@patch('src.investment.one_to_n_funcs.postprocessing_funcs.update_lob_for_duals')
@patch('src.investment.one_to_n_funcs.split_reimb_dates')
def test_one_to_n_cleaning_full_pipeline(self, mock_split_dates, mock_update_duals,
mock_fill_na, mock_get_lob, mock_crosswalk):
"""Test full one-to-n cleaning pipeline."""
input_data = [{"SERVICE_TERM": "Labs", "REIMB_TERM": "100% of Medicare"}]
mock_crosswalk.return_value = input_data
mock_get_lob.return_value = input_data
mock_fill_na.return_value = input_data
mock_update_duals.return_value = input_data
mock_split_dates.return_value = input_data
result = one_to_n_funcs.one_to_n_cleaning(
input_data, self.exhibit_text, self.constants, self.filename
)
# Verify all cleaning steps were called
mock_crosswalk.assert_called_once()
mock_get_lob.assert_called_once()
mock_fill_na.assert_called_once()
mock_update_duals.assert_called_once()
mock_split_dates.assert_called_once()
self.assertEqual(result, input_data)
# ==================== lesser_of_distribution Tests ====================
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_check')
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_distribution')
def test_lesser_of_distribution_with_lesser_term_not_overarching(self, mock_lesser_dist, mock_lesser_check):
"""Test lesser_of_distribution when 'less' is in term but not overarching."""
mock_lesser_check.return_value = False # Not overarching
input_answers = [
{"SERVICE_TERM": "Labs", "REIMB_TERM": "lesser of Medicare or billed charges"}
]
result = one_to_n_funcs.lesser_of_distribution(
input_answers, "exhibit text", "1", self.constants, self.filename
)
self.assertEqual(len(result), 1)
self.assertEqual(result[0]["REIMB_TERM"], "lesser of Medicare or billed charges")
mock_lesser_check.assert_called_once()
mock_lesser_dist.assert_not_called()
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_check')
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_distribution')
def test_lesser_of_distribution_with_lesser_term_overarching(self, mock_lesser_dist, mock_lesser_check):
"""Test lesser_of_distribution when 'less' is in term and is overarching."""
mock_lesser_check.return_value = True # Is overarching
input_answers = [
{"SERVICE_TERM": "Labs", "REIMB_TERM": "lesser of Medicare or billed charges"}
]
result = one_to_n_funcs.lesser_of_distribution(
input_answers, "exhibit text", "1", self.constants, self.filename
)
# When overarching, the answer should not be included (filtered out)
self.assertEqual(len(result), 0)
mock_lesser_check.assert_called_once()
mock_lesser_dist.assert_not_called()
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_check')
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_distribution')
def test_lesser_of_distribution_without_lesser_term(self, mock_lesser_dist, mock_lesser_check):
"""Test lesser_of_distribution when 'less' is not in term."""
mock_lesser_dist.return_value = "100% of Medicare fee schedule"
input_answers = [
{"SERVICE_TERM": "Labs", "REIMB_TERM": "100% of Medicare"}
]
result = one_to_n_funcs.lesser_of_distribution(
input_answers, "exhibit text", "1", self.constants, self.filename
)
self.assertEqual(len(result), 1)
self.assertEqual(result[0]["REIMB_TERM"], "100% of Medicare fee schedule")
mock_lesser_check.assert_not_called()
mock_lesser_dist.assert_called_once_with(
"Labs", "100% of Medicare", "exhibit text", "1", self.filename
)
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_check')
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_distribution')
def test_lesser_of_distribution_multiple_answers(self, mock_lesser_dist, mock_lesser_check):
"""Test lesser_of_distribution with multiple reimbursement answers."""
mock_lesser_check.return_value = False
mock_lesser_dist.side_effect = [
"Updated term 1",
"Updated term 2"
]
input_answers = [
{"SERVICE_TERM": "Labs", "REIMB_TERM": "lesser of A or B"},
{"SERVICE_TERM": "X-Ray", "REIMB_TERM": "Fee schedule"},
{"SERVICE_TERM": "Surgery", "REIMB_TERM": "Per diem"}
]
result = one_to_n_funcs.lesser_of_distribution(
input_answers, "exhibit text", "1", self.constants, self.filename
)
self.assertEqual(len(result), 3)
# First answer has "less" but not overarching, so kept as-is
self.assertEqual(result[0]["REIMB_TERM"], "lesser of A or B")
# Second and third answers get updated via prompt_lesser_of_distribution
self.assertEqual(result[1]["REIMB_TERM"], "Updated term 1")
self.assertEqual(result[2]["REIMB_TERM"], "Updated term 2")
self.assertEqual(mock_lesser_check.call_count, 1)
self.assertEqual(mock_lesser_dist.call_count, 2)
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_check')
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_distribution')
def test_lesser_of_distribution_case_insensitive_less(self, mock_lesser_dist, mock_lesser_check):
"""Test that 'less' detection is case-insensitive."""
mock_lesser_check.return_value = False
input_answers = [
{"SERVICE_TERM": "Labs", "REIMB_TERM": "LESSER of Medicare or billed"},
{"SERVICE_TERM": "X-Ray", "REIMB_TERM": "Less of A or B"},
{"SERVICE_TERM": "Surgery", "REIMB_TERM": "LESS of X or Y"}
]
result = one_to_n_funcs.lesser_of_distribution(
input_answers, "exhibit text", "1", self.constants, self.filename
)
# All three should trigger lesser_of_check (case-insensitive)
self.assertEqual(mock_lesser_check.call_count, 3)
self.assertEqual(len(result), 3)
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_check')
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_distribution')
def test_lesser_of_distribution_empty_input(self, mock_lesser_dist, mock_lesser_check):
"""Test lesser_of_distribution with empty input list."""
result = one_to_n_funcs.lesser_of_distribution(
[], "exhibit text", "1", self.constants, self.filename
)
self.assertEqual(result, [])
mock_lesser_check.assert_not_called()
mock_lesser_dist.assert_not_called()
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_check')
@patch('src.investment.one_to_n_funcs.prompt_calls.prompt_lesser_of_distribution')
def test_lesser_of_distribution_return_type(self, mock_lesser_dist, mock_lesser_check):
"""Test that lesser_of_distribution returns correct type structure."""
mock_lesser_dist.return_value = "Updated term"
input_answers = [
{"SERVICE_TERM": "Labs", "REIMB_TERM": "100% of Medicare"}
]
result = one_to_n_funcs.lesser_of_distribution(
input_answers, "exhibit text", "1", self.constants, self.filename
)
# Verify return type is list[dict[str, str]]
self.assertIsInstance(result, list)
self.assertGreater(len(result), 0)
self.assertIsInstance(result[0], dict)
self.assertIn("SERVICE_TERM", result[0])
self.assertIn("REIMB_TERM", result[0])
if __name__ == "__main__":
unittest.main()