From 8fbe64e42e27c6e327df601e312369d8cea23abb Mon Sep 17 00:00:00 2001 From: Siddhant Medar Date: Tue, 3 Feb 2026 17:59:01 +0000 Subject: [PATCH] Merged in feature/adding_contract_admenment_num_pc (pull request #863) Feature/adding contract admenment num pc * Updated contract amendment * Format code with Black * updated to have letters * format fixes * Adjusted unit testing * Merge remote-tracking branch 'origin/DEV' into feature/adding_contract_admenment_num_pc * apply formatting --- src/constants/parent_child/bcbs.py | 1 + src/parent_child/__main__.py | 1 - src/parent_child/bcbsnc/mapping.py | 7 +- src/parent_child/column_mapper.py | 7 + src/parent_child/pipeline.py | 28 ++- src/parent_child/qc.py | 44 +++- src/prompts/prompt_templates.py | 1 + src/tests/test_parent_child.py | 373 ++++++++++++++++++++++++++++- 8 files changed, 445 insertions(+), 17 deletions(-) diff --git a/src/constants/parent_child/bcbs.py b/src/constants/parent_child/bcbs.py index f3d20b5..1d2dfd0 100644 --- a/src/constants/parent_child/bcbs.py +++ b/src/constants/parent_child/bcbs.py @@ -42,6 +42,7 @@ COL_EFF_DATE_RANK = "effective_date_rank" COL_FINAL_RANK = "final_rank" COL_CHILD_RANK = "child_rank" COL_CHILD_INDEX = "child_index" +COL_AMENDMENT_NUM = "AARETE_DERIVED_AMENDMENT_NUM" # Crosswalk columns XWALK_COL_FILE_NAME = "File Name" diff --git a/src/parent_child/__main__.py b/src/parent_child/__main__.py index 1ef203a..6b6a257 100644 --- a/src/parent_child/__main__.py +++ b/src/parent_child/__main__.py @@ -172,7 +172,6 @@ if __name__ == "__main__": ) # Generate timestamp for this run run_timestamp = f"run_{datetime.now().strftime('%Y%m%d_%H-%M')}_{config.BATCH_ID}" - if config.DOCZY_OUTPUT_FOR_PC: results = main(config.DOCZY_OUTPUT_FOR_PC) logging.info(f"PC Mapping complete. Processed {results} files.") diff --git a/src/parent_child/bcbsnc/mapping.py b/src/parent_child/bcbsnc/mapping.py index f31c4c0..e9ed8e9 100644 --- a/src/parent_child/bcbsnc/mapping.py +++ b/src/parent_child/bcbsnc/mapping.py @@ -57,6 +57,7 @@ from src.constants.parent_child.bcbs import ( NON_PARENT_KEYWORDS, DATE_PATTERNS, IRS_GROUP_REPLACEMENTS, + COL_AMENDMENT_NUM, ) logger = logging.getLogger(__name__) @@ -91,6 +92,7 @@ OUTPUT_COLUMNS = [ COL_CONTRACT_EFF_DATE, "File Name", "Provider Type_Consolidated", + "AARETE_DERIVED_AMENDMENT_NUM", ] # ============================================================================ @@ -1025,9 +1027,12 @@ def bcbs_main(df_read, xwalk_path=None): # Step 28: Add File Name and Provider Type_Consolidated columns subset["File Name"] = subset[COL_CONTRACT_NAME] subset["Provider Type_Consolidated"] = subset[COL_CONSOLIDATED_PROVIDER] + if COL_AMENDMENT_NUM in subset.columns: + subset["AARETE_DERIVED_AMENDMENT_NUM"] = subset[COL_AMENDMENT_NUM] # Step 29: Select output columns and export - subset = subset[OUTPUT_COLUMNS] + output_cols = [col for col in OUTPUT_COLUMNS if col in subset.columns] + subset = subset[output_cols] subset = subset.sort_values(COL_FOLDER) logger.info(f"Total records: {len(subset)}") logger.info(f"Parents: {subset[COL_IS_PARENT].sum()}") diff --git a/src/parent_child/column_mapper.py b/src/parent_child/column_mapper.py index c837ca5..c77abe0 100644 --- a/src/parent_child/column_mapper.py +++ b/src/parent_child/column_mapper.py @@ -34,6 +34,7 @@ class ColumnMapper: r"^contract.*title$", r"^title$", r"^contract.*name.*title$", + r"^contract.*name$", r"^agreement.*title$", r"^doc.*title$", ], @@ -51,6 +52,7 @@ class ColumnMapper: r"^provider.*name$", r"^prov.*name$", r"^group.*name$", + r"^multiple.*irs.*names$", ], "PROV_GROUP_TIN": [ r"^prov.*group.*tin$", @@ -105,6 +107,10 @@ class ColumnMapper: r"^plan.*state$", r"^health.*plan.*state$", ], + "AARETE_DERIVED_AMENDMENT_NUM": [ + r"aarete_derived_amendment_num", + r"^amendment.*number$", + ], } # Optional columns (won't fail if not found) @@ -113,6 +119,7 @@ class ColumnMapper: "SERVICE_TERM", "PROV_GROUP_TIN", "PROV_GROUP_NPI", + "AARETE_DERIVED_AMENDMENT_NUM", ] def __init__(self, df: pd.DataFrame): diff --git a/src/parent_child/pipeline.py b/src/parent_child/pipeline.py index cc1ce27..441adcc 100644 --- a/src/parent_child/pipeline.py +++ b/src/parent_child/pipeline.py @@ -771,35 +771,45 @@ def assign_child_ranks(df, grouper_col="grouping_key"): children.at[idx, "_parent_identity"] = f"unknown_{assigned_rank}" # Group by parent identity and assign sequential ranks + has_amendment_col = "AARETE_DERIVED_AMENDMENT_NUM" in children.columns + for parent_id, grp_df in children.groupby("_parent_identity"): if parent_id == "orphan": - base_rank = "0.0" + parent_rank_str = "0" else: # Extract parent_rank from the child's assigned_parent_rank assigned_rank = grp_df.iloc[0]["assigned_parent_rank"] - # Convert to int if it's a float (2.0 -> 2), then add .0 + # Convert to int if it's a float (2.0 -> 2) if pd.notna(assigned_rank) and assigned_rank != ASSIGNMENT_NO_PARENT: try: if isinstance(assigned_rank, float): - base_rank = f"{int(assigned_rank)}.0" + parent_rank_str = str(int(assigned_rank)) else: - base_rank = f"{assigned_rank}.0" + parent_rank_str = str(assigned_rank) except Exception as e: logging.debug( f"Could not convert rank '{assigned_rank}' to int: {e}" ) - base_rank = f"{assigned_rank}.0" + parent_rank_str = str(assigned_rank) else: - base_rank = "0.0" + parent_rank_str = "0" # Sort by effective date (already converted to datetime at function start) grp_df = grp_df.copy() grp_df["sort_date"] = grp_df["fixed_effective_date"].fillna(pd.Timestamp.max) grp_df = grp_df.sort_values("sort_date") - # Assign sequential ranks + # Assign sequential ranks using amendment number instead of 0 for i, idx in enumerate(grp_df.index, start=1): - rank_val = f"{base_rank}.{i}" + amendment_val = 0 + if has_amendment_col: + raw = grp_df.at[idx, "AARETE_DERIVED_AMENDMENT_NUM"] + if pd.notna(raw) and str(raw).strip() != "": + try: + amendment_val = int(float(raw)) + except (ValueError, TypeError): + amendment_val = raw + rank_val = f"{parent_rank_str}.{amendment_val}.{i}" children.loc[idx, "child_rank"] = rank_val # Clean up temp column @@ -1044,6 +1054,7 @@ def parent_child_mapping( cols_order = [ "grouping_key", "PROV_GROUP_NAME_FULL_cleaned", + "AARETE_DERIVED_AMENDMENT_NUM", "consolidated_lob", "fixed_effective_date", "parent_child_flag", @@ -1110,6 +1121,7 @@ def parent_child_mapping( "PROV_GROUP_NPI", "payer_name_cleaned", "PAYER_STATE", + "AARETE_DERIVED_AMENDMENT_NUM", "grouping_key", "parent", "combined_rank", diff --git a/src/parent_child/qc.py b/src/parent_child/qc.py index 31388a9..fa95510 100644 --- a/src/parent_child/qc.py +++ b/src/parent_child/qc.py @@ -1793,6 +1793,7 @@ class ConfigFactory: "cnc": ConfigFactory.create_molina, # CNC uses same config as Molina "caresource": ConfigFactory.create_caresource, "bcbs": ConfigFactory.create_bcbs, + "bcbsnc": ConfigFactory.create_bcbs, "clover_health": ConfigFactory.create_clover, } @@ -2020,6 +2021,20 @@ class ParentChildEngine: parent_cache = df.loc[df["is_parent"], cols_needed].to_dict("index") return self.child_assigner.assign_children(df, self.cfg, parent_cache) + @staticmethod + def _normalize_amendment_val(x): + """Convert amendment value to string for rank construction. + + Handles both numeric values (e.g. 3.0 -> '3') and character + values (e.g. 'A' -> 'A'). Returns '0' for NaN / empty. + """ + if pd.notna(x) and str(x).strip() != "": + try: + return str(int(float(x))) + except (ValueError, TypeError): + return str(x).strip() + return "0" + def prepare_output(self, df: pd.DataFrame) -> pd.DataFrame: """Initialize output columns.""" df["parent_child_flag_dest"] = pd.Series([pd.NA] * len(df), dtype="string") @@ -2060,12 +2075,23 @@ class ParentChildEngine: parent_idx = tmp["assigned_parent_idx"].astype("int64").values parent_rank_str = ( df.loc[parent_idx, "parent_rank"] - .astype("string") + .apply(lambda x: str(int(x)) if pd.notna(x) else "0") .reset_index(drop=True) .values ) - df.loc[tmp.index, "child_rank_dest"] = parent_rank_str + ".0." + seq_str + if "AARETE_DERIVED_AMENDMENT_NUM" in df.columns: + amendment_str = ( + df.loc[tmp.index, "AARETE_DERIVED_AMENDMENT_NUM"] + .apply(self._normalize_amendment_val) + .reset_index(drop=True) + .values + ) + else: + amendment_str = "0" + df.loc[tmp.index, "child_rank_dest"] = ( + parent_rank_str + "." + amendment_str + "." + seq_str + ) df.loc[tmp.index, "combined_rank_dest"] = df.loc[ tmp.index, "child_rank_dest" ] @@ -2079,9 +2105,17 @@ class ParentChildEngine: tmp = df.loc[orphan_mask, ["input_row_order"]].copy() tmp = tmp.sort_values(["input_row_order"], kind="mergesort") seq = pd.Series(np.arange(1, len(tmp) + 1, dtype=np.int32), index=tmp.index) - df.loc[tmp.index, "orphan_rank_dest"] = ( - "0.0." + seq.astype("string") - ).values + if "AARETE_DERIVED_AMENDMENT_NUM" in df.columns: + amendment_str = df.loc[tmp.index, "AARETE_DERIVED_AMENDMENT_NUM"].apply( + self._normalize_amendment_val + ) + df.loc[tmp.index, "orphan_rank_dest"] = ( + "0." + amendment_str + "." + seq.astype("string") + ).values + else: + df.loc[tmp.index, "orphan_rank_dest"] = ( + "0.0." + seq.astype("string") + ).values df.loc[tmp.index, "combined_rank_dest"] = df.loc[ tmp.index, "orphan_rank_dest" ] diff --git a/src/prompts/prompt_templates.py b/src/prompts/prompt_templates.py index 2f6679e..dd22303 100644 --- a/src/prompts/prompt_templates.py +++ b/src/prompts/prompt_templates.py @@ -1552,6 +1552,7 @@ Return NO if EITHER term contains (even if valid payment method exists): """ + def VALIDATE_REIMBURSEMENTS_PROMPT(service_term: str, reimb_term: str) -> str: """Returns ONLY dynamic content for validation. Call VALIDATE_REIMBURSEMENTS_INSTRUCTION() separately for the cached instruction. diff --git a/src/tests/test_parent_child.py b/src/tests/test_parent_child.py index 0f89a80..48e27f0 100644 --- a/src/tests/test_parent_child.py +++ b/src/tests/test_parent_child.py @@ -34,6 +34,7 @@ from src.parent_child.qc import ( resolve_column, TextProcessor, ConfigFactory, + ParentChildEngine, ) @@ -526,7 +527,8 @@ class TestAssignChildRanks(unittest.TestCase): self.assertTrue(result["child_rank"].iloc[1].startswith("0.0")) def test_assigned_children_get_parent_prefix(self): - """Children assigned to parent get ranks with parent's rank prefix.""" + """Children assigned to parent get ranks with parent's rank prefix. + Without AARETE_DERIVED_AMENDMENT_NUM column, amendment defaults to 0.""" df = pd.DataFrame( { "grouping_key": ["TIN:123", "TIN:123", "TIN:123"], @@ -540,7 +542,7 @@ class TestAssignChildRanks(unittest.TestCase): # Parent should have combined_rank "1" self.assertEqual(result["combined_rank"].iloc[0], "1") - # Children should have ranks like "1.0.1", "1.0.2" + # Children should have ranks like "1.0.1", "1.0.2" (0 = default amendment) child_ranks = result[result["parent"] == False]["child_rank"].tolist() self.assertTrue(all(r.startswith("1.0.") for r in child_ranks)) @@ -637,5 +639,372 @@ class TestConfigFactory(unittest.TestCase): self.assertIsNotNone(config) +class TestAssignChildRanksWithAmendmentNum(unittest.TestCase): + """Tests for assign_child_ranks with AARETE_DERIVED_AMENDMENT_NUM column.""" + + def test_children_use_amendment_num_in_rank(self): + """Children with amendment number get parent_rank.amendment_num.seq format.""" + df = pd.DataFrame( + { + "grouping_key": ["TIN:123", "TIN:123", "TIN:123"], + "parent": [True, False, False], + "parent_rank": [1.0, np.nan, np.nan], + "assigned_parent_rank": [np.nan, 1.0, 1.0], + "fixed_effective_date": ["2024-01-01", "2024-03-01", "2024-02-01"], + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 5, 3], + } + ) + result = assign_child_ranks(df) + + child_df = result[result["parent"] == False].sort_values("fixed_effective_date") + ranks = child_df["child_rank"].tolist() + # Earlier date (index 2, amendment=3) sorted first: 1.3.1 + # Later date (index 1, amendment=5) sorted second: 1.5.2 + self.assertEqual(ranks[0], "1.3.1") + self.assertEqual(ranks[1], "1.5.2") + + def test_children_with_nan_amendment_default_to_0(self): + """Children with NaN/empty amendment number default to 0.""" + df = pd.DataFrame( + { + "grouping_key": ["TIN:123", "TIN:123", "TIN:123"], + "parent": [True, False, False], + "parent_rank": [1.0, np.nan, np.nan], + "assigned_parent_rank": [np.nan, 1.0, 1.0], + "fixed_effective_date": ["2024-01-01", "2024-03-01", "2024-02-01"], + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, np.nan, ""], + } + ) + result = assign_child_ranks(df) + + child_df = result[result["parent"] == False].sort_values("fixed_effective_date") + ranks = child_df["child_rank"].tolist() + # Both should default to 0 for amendment part + self.assertEqual(ranks[0], "1.0.1") + self.assertEqual(ranks[1], "1.0.2") + + def test_orphans_use_amendment_num_in_rank(self): + """Orphans with amendment number get 0.amendment_num.seq format.""" + df = pd.DataFrame( + { + "grouping_key": ["TIN:123", "TIN:123"], + "parent": [False, False], + "assigned_parent_rank": ["no_parent", "no_parent"], + "fixed_effective_date": ["2024-01-01", "2024-02-01"], + "AARETE_DERIVED_AMENDMENT_NUM": [2, 7], + } + ) + result = assign_child_ranks(df) + + ranks = result["child_rank"].tolist() + self.assertEqual(ranks[0], "0.2.1") + self.assertEqual(ranks[1], "0.7.2") + + def test_mixed_amendment_values_in_same_group(self): + """Different amendment numbers within same parent group.""" + df = pd.DataFrame( + { + "grouping_key": ["TIN:123", "TIN:123", "TIN:123", "TIN:123"], + "parent": [True, False, False, False], + "parent_rank": [1.0, np.nan, np.nan, np.nan], + "assigned_parent_rank": [np.nan, 1.0, 1.0, 1.0], + "fixed_effective_date": [ + "2024-01-01", + "2024-02-01", + "2024-03-01", + "2024-04-01", + ], + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 3, np.nan, 5], + } + ) + result = assign_child_ranks(df) + + child_df = result[result["parent"] == False].sort_values("fixed_effective_date") + ranks = child_df["child_rank"].tolist() + self.assertEqual(ranks[0], "1.3.1") # amendment=3 + self.assertEqual(ranks[1], "1.0.2") # amendment=NaN -> 0 + self.assertEqual(ranks[2], "1.5.3") # amendment=5 + + def test_no_amendment_column_backward_compatible(self): + """Without AARETE_DERIVED_AMENDMENT_NUM column, uses 0 (backward compat).""" + df = pd.DataFrame( + { + "grouping_key": ["TIN:123", "TIN:123"], + "parent": [True, False], + "parent_rank": [1.0, np.nan], + "assigned_parent_rank": [np.nan, 1.0], + "fixed_effective_date": ["2024-01-01", "2024-03-01"], + } + ) + result = assign_child_ranks(df) + + child_rank = result[result["parent"] == False]["child_rank"].iloc[0] + self.assertEqual(child_rank, "1.0.1") + + def test_float_amendment_num_converted_to_int(self): + """Float amendment numbers like 3.0 are converted to int 3.""" + df = pd.DataFrame( + { + "grouping_key": ["TIN:123", "TIN:123"], + "parent": [True, False], + "parent_rank": [1.0, np.nan], + "assigned_parent_rank": [np.nan, 1.0], + "fixed_effective_date": ["2024-01-01", "2024-03-01"], + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 3.0], + } + ) + result = assign_child_ranks(df) + + child_rank = result[result["parent"] == False]["child_rank"].iloc[0] + self.assertEqual(child_rank, "1.3.1") + + def test_parent_combined_rank_unchanged(self): + """Parent combined_rank should still be just the parent rank number.""" + df = pd.DataFrame( + { + "grouping_key": ["TIN:123", "TIN:123"], + "parent": [True, False], + "parent_rank": [1.0, np.nan], + "assigned_parent_rank": [np.nan, 1.0], + "fixed_effective_date": ["2024-01-01", "2024-03-01"], + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 5], + } + ) + result = assign_child_ranks(df) + + parent_rank = result[result["parent"] == True]["combined_rank"].iloc[0] + self.assertEqual(parent_rank, "1") + + def test_children_use_character_amendment_num_in_rank(self): + """Children with character amendment number (A, B, C) use it in rank.""" + df = pd.DataFrame( + { + "grouping_key": ["TIN:123", "TIN:123", "TIN:123"], + "parent": [True, False, False], + "parent_rank": [1.0, np.nan, np.nan], + "assigned_parent_rank": [np.nan, 1.0, 1.0], + "fixed_effective_date": ["2024-01-01", "2024-03-01", "2024-02-01"], + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, "B", "A"], + } + ) + result = assign_child_ranks(df) + + child_df = result[result["parent"] == False].sort_values("fixed_effective_date") + ranks = child_df["child_rank"].tolist() + self.assertEqual(ranks[0], "1.A.1") # Amendment A, sequence 1 + self.assertEqual(ranks[1], "1.B.2") # Amendment B, sequence 2 + + def test_orphans_use_character_amendment_num_in_rank(self): + """Orphans with character amendment number get 0.amendment.seq format.""" + df = pd.DataFrame( + { + "grouping_key": ["TIN:123", "TIN:123"], + "parent": [False, False], + "assigned_parent_rank": ["no_parent", "no_parent"], + "fixed_effective_date": ["2024-01-01", "2024-02-01"], + "AARETE_DERIVED_AMENDMENT_NUM": ["A", "C"], + } + ) + result = assign_child_ranks(df) + + ranks = result["child_rank"].tolist() + self.assertEqual(ranks[0], "0.A.1") + self.assertEqual(ranks[1], "0.C.2") + + def test_mixed_numeric_and_character_amendment_nums(self): + """Mix of numeric and character amendment numbers in same group.""" + df = pd.DataFrame( + { + "grouping_key": ["TIN:123", "TIN:123", "TIN:123", "TIN:123"], + "parent": [True, False, False, False], + "parent_rank": [1.0, np.nan, np.nan, np.nan], + "assigned_parent_rank": [np.nan, 1.0, 1.0, 1.0], + "fixed_effective_date": [ + "2024-01-01", + "2024-02-01", + "2024-03-01", + "2024-04-01", + ], + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 3, "A", 5], + } + ) + result = assign_child_ranks(df) + + child_df = result[result["parent"] == False].sort_values("fixed_effective_date") + ranks = child_df["child_rank"].tolist() + self.assertEqual(ranks[0], "1.3.1") # numeric 3 + self.assertEqual(ranks[1], "1.A.2") # character A + self.assertEqual(ranks[2], "1.5.3") # numeric 5 + + +class TestComputeRanksWithAmendmentNum(unittest.TestCase): + """Tests for ParentChildEngine.compute_ranks with AARETE_DERIVED_AMENDMENT_NUM.""" + + def _make_engine(self): + """Create a minimal ParentChildEngine for testing compute_ranks.""" + config = ConfigFactory.get_config("molina") + return ParentChildEngine(config.to_dict()) + + def _make_base_df(self, extra_cols=None): + """Create a base DataFrame with parent, child, and orphan rows. + + Index 0: Parent (parent_rank=1) + Index 1: Child assigned to parent 0 + Index 2: Child assigned to parent 0 + Index 3: Orphan + """ + df = pd.DataFrame( + { + "is_parent": [True, False, False, False], + "parent_rank": [1, np.nan, np.nan, np.nan], + "assigned_parent_idx": [pd.NA, 0, 0, pd.NA], + "eff_date": pd.to_datetime( + ["2024-01-01", "2024-03-01", "2024-02-01", "2024-04-01"] + ), + "input_row_order": [0, 1, 2, 3], + } + ) + df["assigned_parent_idx"] = df["assigned_parent_idx"].astype("Int64") + if extra_cols: + for col, vals in extra_cols.items(): + df[col] = vals + + # Initialize output columns + df["parent_child_flag_dest"] = pd.Series([pd.NA] * len(df), dtype="string") + df["parent_rank_dest"] = pd.Series([pd.NA] * len(df), dtype="string") + df["child_rank_dest"] = pd.Series([pd.NA] * len(df), dtype="string") + df["orphan_rank_dest"] = pd.Series([pd.NA] * len(df), dtype="string") + df["combined_rank_dest"] = pd.Series([pd.NA] * len(df), dtype="string") + + return df + + def test_child_rank_dest_with_amendment_num(self): + """child_rank_dest uses amendment number instead of 0.""" + engine = self._make_engine() + df = self._make_base_df( + extra_cols={ + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 5, 3, 2], + } + ) + result = engine.compute_ranks(df) + + # Children sorted by (assigned_parent_idx, eff_date, input_row_order) + # Index 2 (eff 2024-02-01, amendment=3) comes first -> seq 1 + # Index 1 (eff 2024-03-01, amendment=5) comes second -> seq 2 + self.assertEqual(result.at[2, "child_rank_dest"], "1.3.1") + self.assertEqual(result.at[1, "child_rank_dest"], "1.5.2") + + def test_child_rank_dest_without_amendment_column(self): + """child_rank_dest defaults to 0 when column is missing. + Format: parent_rank.0.seq (backward compatible).""" + engine = self._make_engine() + df = self._make_base_df() # No amendment column + result = engine.compute_ranks(df) + + self.assertEqual(result.at[2, "child_rank_dest"], "1.0.1") + self.assertEqual(result.at[1, "child_rank_dest"], "1.0.2") + + def test_child_rank_dest_with_nan_amendment(self): + """child_rank_dest defaults to 0 for NaN amendment values.""" + engine = self._make_engine() + df = self._make_base_df( + extra_cols={ + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, np.nan, np.nan, np.nan], + } + ) + result = engine.compute_ranks(df) + + # NaN amendment defaults to 0, so same as without the column + self.assertEqual(result.at[2, "child_rank_dest"], "1.0.1") + self.assertEqual(result.at[1, "child_rank_dest"], "1.0.2") + + def test_orphan_rank_dest_with_amendment_num(self): + """orphan_rank_dest uses amendment number instead of 0.""" + engine = self._make_engine() + df = self._make_base_df( + extra_cols={ + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 5, 3, 7], + } + ) + result = engine.compute_ranks(df) + + # Index 3 is orphan with amendment=7 + self.assertEqual(result.at[3, "orphan_rank_dest"], "0.7.1") + + def test_orphan_rank_dest_without_amendment_column(self): + """orphan_rank_dest defaults to 0 when column is missing.""" + engine = self._make_engine() + df = self._make_base_df() # No amendment column + result = engine.compute_ranks(df) + + self.assertEqual(result.at[3, "orphan_rank_dest"], "0.0.1") + + def test_combined_rank_dest_matches_child_and_orphan(self): + """combined_rank_dest equals child_rank_dest for children and orphan_rank_dest for orphans.""" + engine = self._make_engine() + df = self._make_base_df( + extra_cols={ + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 5, 3, 7], + } + ) + result = engine.compute_ranks(df) + + # Children: combined_rank_dest == child_rank_dest + self.assertEqual( + result.at[1, "combined_rank_dest"], result.at[1, "child_rank_dest"] + ) + self.assertEqual( + result.at[2, "combined_rank_dest"], result.at[2, "child_rank_dest"] + ) + # Orphan: combined_rank_dest == orphan_rank_dest + self.assertEqual( + result.at[3, "combined_rank_dest"], result.at[3, "orphan_rank_dest"] + ) + + def test_child_rank_dest_with_character_amendment_num(self): + """child_rank_dest handles character amendment values (A, B, C).""" + engine = self._make_engine() + df = self._make_base_df( + extra_cols={ + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, "B", "A", "C"], + } + ) + result = engine.compute_ranks(df) + + # Index 2 (eff 2024-02-01, amendment=A) comes first -> seq 1 + # Index 1 (eff 2024-03-01, amendment=B) comes second -> seq 2 + self.assertEqual(result.at[2, "child_rank_dest"], "1.A.1") + self.assertEqual(result.at[1, "child_rank_dest"], "1.B.2") + + def test_orphan_rank_dest_with_character_amendment_num(self): + """orphan_rank_dest handles character amendment values.""" + engine = self._make_engine() + df = self._make_base_df( + extra_cols={ + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 5, 3, "A"], + } + ) + result = engine.compute_ranks(df) + + self.assertEqual(result.at[3, "orphan_rank_dest"], "0.A.1") + + def test_child_rank_dest_with_mixed_numeric_and_character_amendment(self): + """child_rank_dest handles mix of numeric and character amendment values.""" + engine = self._make_engine() + df = self._make_base_df( + extra_cols={ + "AARETE_DERIVED_AMENDMENT_NUM": [np.nan, "A", 3, "B"], + } + ) + result = engine.compute_ranks(df) + + # Index 2 (eff 2024-02-01, amendment=3) -> seq 1 + # Index 1 (eff 2024-03-01, amendment=A) -> seq 2 + self.assertEqual(result.at[2, "child_rank_dest"], "1.3.1") + self.assertEqual(result.at[1, "child_rank_dest"], "1.A.2") + # Index 3 is orphan with amendment=B + self.assertEqual(result.at[3, "orphan_rank_dest"], "0.B.1") + + if __name__ == "__main__": unittest.main()