diff --git a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py index 754054b6d..73daa1eeb 100644 --- a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py +++ b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py @@ -426,19 +426,19 @@ def find_best_license_in_current_known_licenses( def generate_privileges_for_provider(self, include_inactive_privileges: bool = False) -> list[dict]: """ - Generate privilege dicts at runtime for all eligible license types this provider holds. + Generate privilege dicts at runtime for each license type this provider holds. For each license type, the home license is chosen from all licenses of that type: the license renewed most recently (when dateOfRenewal is present), otherwise the license with the most recent date of issuance. - By default, privileges are generated only when the chosen home license is compact-eligible. + When the chosen home license is compact-eligible, one privilege is generated per active compact jurisdiction + (excluding the home jurisdiction). When the home license is not compact-eligible, a privilege is still + generated for a jurisdiction if there is a matching privilege adverse action or an open privilege + investigation for that jurisdiction and license type, so admins can see and resolve those records. - When include_inactive_privileges is True, privileges are also generated for ineligible home + When include_inactive_privileges is True, privileges in all jurisdictions are generated for ineligible home licenses and are marked inactive. This is primarily used when indexing to OpenSearch so that adverse actions and investigations remain searchable even when a license is ineligible. - For each qualifying type, one privilege is generated per active compact jurisdiction - (excluding the home jurisdiction). - :param include_inactive_privileges: When True, generate privileges for ineligible home licenses and mark them inactive instead of omitting them entirely. """ @@ -466,16 +466,6 @@ def generate_privileges_for_provider(self, include_inactive_privileges: bool = F reverse=True, ) most_recent_license = sorted_licenses[0] - if ( - not include_inactive_privileges - and most_recent_license.compactEligibility != CompactEligibilityStatus.ELIGIBLE - ): - logger.debug( - 'skipping inactive license', - license_jurisdiction=most_recent_license.jurisdiction, - license_type=most_recent_license.licenseType, - ) - continue most_recent_licenses_for_each_type.append(most_recent_license) result: list[dict] = [] @@ -492,6 +482,20 @@ def generate_privileges_for_provider(self, include_inactive_privileges: bool = F inv_records = self.get_investigation_records_for_privilege( jurisdiction, license_type_abbr, include_closed=False ) + if ( + not is_eligible + and not include_inactive_privileges + and not privilege_aa + and not inv_records + ): + logger.debug('Not returning a privilege for this jurisdiction because the home ' + 'license is not compact eligible and there are no matching privilege adverse ' + 'actions or open investigations.', + jurisdiction=jurisdiction, + home_jurisdiction=home_jurisdiction, + license_type_abbr=license_type_abbr, + ) + continue privilege_dict = { 'type': 'privilege', 'administratorSetStatus': ActiveInactiveStatus.ACTIVE.value, @@ -501,8 +505,10 @@ def generate_privileges_for_provider(self, include_inactive_privileges: bool = F 'licenseJurisdiction': home_jurisdiction, 'licenseType': most_recent_license.licenseType, 'dateOfExpiration': most_recent_license.dateOfExpiration, - # A privilege is inactive if the home license is ineligible, or if a state admin - # has set an encumbrance that has not been lifted. + # the only way a privilege under this model shows inactive is if + # there has been an encumbrance set by a state admin that has not been + # lifted. Ineligible home licenses still get privilege rows when there are matching + # privilege adverse actions or open investigations. 'status': ActiveInactiveStatus.ACTIVE.value if is_eligible and not privilege_unlifted else ActiveInactiveStatus.INACTIVE.value, diff --git a/backend/cosmetology-app/lambdas/python/common/requirements-dev.in b/backend/cosmetology-app/lambdas/python/common/requirements-dev.in index a00545ae5..ff9831436 100644 --- a/backend/cosmetology-app/lambdas/python/common/requirements-dev.in +++ b/backend/cosmetology-app/lambdas/python/common/requirements-dev.in @@ -4,3 +4,4 @@ moto[all]>=5.0.12, <6 boto3-stubs[full] Faker>=37, <38 cryptography>=46, <47 +attrs>=25, <26 diff --git a/backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py b/backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py index e40755cc6..8fc8ae0ed 100644 --- a/backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py +++ b/backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py @@ -367,6 +367,68 @@ def test_open_investigation_included_and_investigation_status_set(self): # even with the investigation status, it should still be set to active self.assertEqual('active', privilege_al['status']) + def test_returns_privilege_when_home_ineligible_and_privilege_adverse_action_matches(self): + """Ineligible home license still yields a privilege row when a privilege AA matches that jurisdiction.""" + from cc_common.data_model.provider_record_util import ProviderUserRecords + from cc_common.data_model.schema.common import CompactEligibilityStatus + + records = self._make_provider_records( + license_overrides_list=[ + { + 'jurisdiction': 'oh', + 'jurisdictionUploadedCompactEligibility': CompactEligibilityStatus.INELIGIBLE, + 'dateOfExpiration': date(2026, 4, 4), + } + ] + ) + records.append( + self.test_data_generator.generate_default_adverse_action( + value_overrides={'jurisdiction': 'al'} + ).serialize_to_database_record() + ) + with self._patch_config_for_privilege_generation(): + pur = ProviderUserRecords(records) + result = pur.generate_privileges_for_provider() + self.assertEqual(1, len(result)) + self.assertEqual('al', result[0]['jurisdiction']) + self.assertGreaterEqual(len(result[0]['adverseActions']), 1) + self.assertEqual({'al'}, {p['jurisdiction'] for p in result}) + + def test_returns_privilege_when_home_ineligible_and_open_privilege_investigation_matches(self): + """Ineligible home license still yields a privilege row when an open privilege investigation matches.""" + from cc_common.data_model.provider_record_util import ProviderUserRecords + from cc_common.data_model.schema.common import CompactEligibilityStatus, InvestigationStatusEnum + + records = self._make_provider_records( + license_overrides_list=[ + { + 'jurisdiction': 'oh', + 'licenseType': 'cosmetologist', + 'jurisdictionUploadedCompactEligibility': CompactEligibilityStatus.INELIGIBLE, + 'dateOfExpiration': date(2026, 4, 4), + } + ] + ) + open_investigation = self.test_data_generator.generate_default_investigation( + value_overrides={ + 'jurisdiction': 'al', + 'licenseTypeAbbreviation': 'cos', + 'licenseType': 'cosmetologist', + 'investigationAgainst': 'privilege', + } + ) + records.append(open_investigation.serialize_to_database_record()) + with self._patch_config_for_privilege_generation(): + pur = ProviderUserRecords(records) + result = pur.generate_privileges_for_provider() + self.assertEqual(1, len(result)) + self.assertEqual('al', result[0]['jurisdiction']) + self.assertEqual(1, len(result[0]['investigations'])) + self.assertEqual( + InvestigationStatusEnum.UNDER_INVESTIGATION.value, + result[0]['investigationStatus'], + ) + class TestProviderRecordUtility(TstLambdas): def setUp(self):