Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
05efa81
Set encumbrance type and categories to Cosmetology values
landonshumway-ia Apr 3, 2026
c0a73a2
replace encumbrance type in tests
landonshumway-ia Apr 3, 2026
9dd0dbd
Enforce exactly one category
landonshumway-ia Apr 3, 2026
17af03f
WIP - refactor smoke tests to get provider data without provider login
landonshumway-ia Apr 3, 2026
1e5c352
Remove unused notification methods
landonshumway-ia Apr 3, 2026
fb591d0
smoke test cleanup
landonshumway-ia Apr 3, 2026
30357f6
fix encumbrance type in smoke test
landonshumway-ia Apr 3, 2026
3187024
Update smoke tests to reference valid cosm member state
landonshumway-ia Apr 3, 2026
378a09d
Add permission to lambda to get live state jurisdiction
landonshumway-ia Apr 3, 2026
3e6caac
Refactor investigation smoke tests to work with multi-state license m…
landonshumway-ia Apr 3, 2026
8f63fc2
Refactor encumbrance smoke tests to work with multi-state license model
landonshumway-ia Apr 3, 2026
ac4d7d9
Add additional state to compact config smoke tests
landonshumway-ia Apr 3, 2026
7003ba7
formatting/linter
landonshumway-ia Apr 3, 2026
b8a2a0a
Update license rollback smoke tests
landonshumway-ia Apr 4, 2026
c6fcf71
Complete smoke test cleanup
landonshumway-ia Apr 4, 2026
e5a061a
Using cached config property to check live jurisdictions
landonshumway-ia Apr 6, 2026
0bf2a88
PR feedback - smoke test refinement
landonshumway-ia Apr 6, 2026
1ad1fe1
PR feedback - fail fast with smoke test check
landonshumway-ia Apr 9, 2026
ef246e7
formatting
landonshumway-ia Apr 9, 2026
b2d5d31
PR feedback - fix smoke test print statement
landonshumway-ia Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -482,18 +482,14 @@ 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,
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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ class AdverseActionPostRequestSchema(ForgivingSchema):

encumbranceEffectiveDate = Date(required=True, allow_none=False)
encumbranceType = EncumbranceTypeField(required=True, allow_none=False)
# in the case of Cosmetology, we only allow one category, but we are keeping this as a list for compatibility
# with the existing code base, and to allow the potential of supporting multiple categories should this be needed
# in the future
clinicalPrivilegeActionCategories = List(
ClinicalPrivilegeActionCategoryField(), required=True, allow_none=False, validate=Length(min=1)
ClinicalPrivilegeActionCategoryField(), required=True, allow_none=False, validate=Length(equal=1)
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,37 +378,17 @@ class EncumbranceType(CCEnum):
Enum for the allowed types of encumbrances
"""

FINE = 'fine'
REPRIMAND = 'reprimand'
REQUIRED_SUPERVISION = 'required supervision'
COMPLETION_OF_CONTINUING_EDUCATION = 'completion of continuing education'
PUBLIC_REPRIMAND = 'public reprimand'
PROBATION = 'probation'
INJUNCTIVE_ACTION = 'injunctive action'
SUSPENSION = 'suspension'
REVOCATION = 'revocation'
DENIAL = 'denial'
SURRENDER_OF_LICENSE = 'surrender of license'
MODIFICATION_OF_PREVIOUS_ACTION_EXTENSION = 'modification of previous action-extension'
MODIFICATION_OF_PREVIOUS_ACTION_REDUCTION = 'modification of previous action-reduction'
OTHER_MONITORING = 'other monitoring'
OTHER_ADJUDICATED_ACTION_NOT_LISTED = 'other adjudicated action not listed'


class ClinicalPrivilegeActionCategory(CCEnum):
"""
Enum for the category of clinical privileges actions, as defined by NPDB:
https://www.npdb.hrsa.gov/software/CodeLists.pdf, Tables 41-45
"""
"""Enum for adverse action clinical privilege action categories."""

NON_COMPLIANCE = 'Non-compliance With Requirements'
CRIMINAL_CONVICTION = 'Criminal Conviction or Adjudication'
CONFIDENTIALITY_VIOLATION = 'Confidentiality, Consent or Disclosure Violations'
MISCONDUCT_ABUSE = 'Misconduct or Abuse'
FRAUD = 'Fraud, Deception, or Misrepresentation'
UNSAFE_PRACTICE = 'Unsafe Practice or Substandard Care'
IMPROPER_SUPERVISION = 'Improper Supervision or Allowing Unlicensed Practice'
OTHER = 'Other'
FRAUD = 'fraud'
CONSUMER_HARM = 'consumer harm'
OTHER = 'other'


class ChangeHashMixin:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,36 +91,6 @@ def _invoke_lambda(self, payload: dict[str, Any]) -> dict[str, Any]:
self._logger.error(error_message, payload=payload, exception=str(e))
raise CCInternalException(error_message) from e

def send_license_encumbrance_provider_notification_email(
self,
*,
compact: str,
provider_email: str,
template_variables: EncumbranceNotificationTemplateVariables,
) -> dict[str, str]:
"""
Send a license encumbrance notification email to a provider.

:param compact: Compact name
:param provider_email: Email address of the provider
:param template_variables: Template variables for the email
:return: Response from the email notification service
"""
payload = {
'compact': compact,
'template': 'licenseEncumbranceProviderNotification',
'recipientType': 'SPECIFIC',
'specificEmails': [provider_email],
'templateVariables': {
'providerFirstName': template_variables.provider_first_name,
'providerLastName': template_variables.provider_last_name,
'encumberedJurisdiction': template_variables.encumbered_jurisdiction,
'licenseType': template_variables.license_type,
'effectiveStartDate': template_variables.effective_date.strftime('%B %d, %Y'),
},
}
return self._invoke_lambda(payload)

def send_license_encumbrance_state_notification_email(
self,
*,
Expand Down Expand Up @@ -155,36 +125,6 @@ def send_license_encumbrance_state_notification_email(
}
return self._invoke_lambda(payload)

def send_license_encumbrance_lifting_provider_notification_email(
self,
*,
compact: str,
provider_email: str,
template_variables: EncumbranceNotificationTemplateVariables,
) -> dict[str, str]:
"""
Send a license encumbrance lifting notification email to a provider.

:param compact: Compact name
:param provider_email: Email address of the provider
:param template_variables: Template variables for the email
:return: Response from the email notification service
"""
payload = {
'compact': compact,
'template': 'licenseEncumbranceLiftingProviderNotification',
'recipientType': 'SPECIFIC',
'specificEmails': [provider_email],
'templateVariables': {
'providerFirstName': template_variables.provider_first_name,
'providerLastName': template_variables.provider_last_name,
'liftedJurisdiction': template_variables.encumbered_jurisdiction,
'licenseType': template_variables.license_type,
'effectiveLiftDate': template_variables.effective_date.strftime('%B %d, %Y'),
},
}
return self._invoke_lambda(payload)

def send_license_encumbrance_lifting_state_notification_email(
self,
*,
Expand Down Expand Up @@ -219,36 +159,6 @@ def send_license_encumbrance_lifting_state_notification_email(
}
return self._invoke_lambda(payload)

def send_privilege_encumbrance_provider_notification_email(
self,
*,
compact: str,
provider_email: str,
template_variables: EncumbranceNotificationTemplateVariables,
) -> dict[str, str]:
"""
Send a privilege encumbrance notification email to a provider.

:param compact: Compact name
:param provider_email: Email address of the provider
:param template_variables: Template variables for the email
:return: Response from the email notification service
"""
payload = {
'compact': compact,
'template': 'privilegeEncumbranceProviderNotification',
'recipientType': 'SPECIFIC',
'specificEmails': [provider_email],
'templateVariables': {
'providerFirstName': template_variables.provider_first_name,
'providerLastName': template_variables.provider_last_name,
'encumberedJurisdiction': template_variables.encumbered_jurisdiction,
'licenseType': template_variables.license_type,
'effectiveStartDate': template_variables.effective_date.strftime('%B %d, %Y'),
},
}
return self._invoke_lambda(payload)

def send_privilege_encumbrance_state_notification_email(
self,
*,
Expand Down Expand Up @@ -283,36 +193,6 @@ def send_privilege_encumbrance_state_notification_email(
}
return self._invoke_lambda(payload)

def send_privilege_encumbrance_lifting_provider_notification_email(
self,
*,
compact: str,
provider_email: str,
template_variables: EncumbranceNotificationTemplateVariables,
) -> dict[str, str]:
"""
Send a privilege encumbrance lifting notification email to a provider.

:param compact: Compact name
:param provider_email: Email address of the provider
:param template_variables: Template variables for the email
:return: Response from the email notification service
"""
payload = {
'compact': compact,
'template': 'privilegeEncumbranceLiftingProviderNotification',
'recipientType': 'SPECIFIC',
'specificEmails': [provider_email],
'templateVariables': {
'providerFirstName': template_variables.provider_first_name,
'providerLastName': template_variables.provider_last_name,
'liftedJurisdiction': template_variables.encumbered_jurisdiction,
'licenseType': template_variables.license_type,
'effectiveLiftDate': template_variables.effective_date.strftime('%B %d, %Y'),
},
}
return self._invoke_lambda(payload)

def send_privilege_encumbrance_lifting_state_notification_email(
self,
*,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
DEFAULT_ACTION_AGAINST_PRIVILEGE = 'privilege'
DEFAULT_BLOCKS_FUTURE_PRIVILEGES = True
DEFAULT_ENCUMBRANCE_TYPE = 'suspension'
DEFAULT_CLINICAL_PRIVILEGE_ACTION_CATEGORY = 'Unsafe Practice or Substandard Care'
DEFAULT_CLINICAL_PRIVILEGE_ACTION_CATEGORY = 'fraud'
DEFAULT_CREATION_EFFECTIVE_DATE = '2024-02-15'
DEFAULT_CREATION_DATE = '2024-02-15T10:30:00+00:00'
DEFAULT_AA_SUBMITTING_USER_ID = '12a6377e-c3a5-40e5-bca5-317ec854c556'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"encumbranceEffectiveDate": "2023-01-15",
"encumbranceType": "suspension",
"clinicalPrivilegeActionCategories": ["Unsafe Practice or Substandard Care"]
"clinicalPrivilegeActionCategories": ["fraud"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def test_adverse_action_data_class_outputs_expected_database_object(self):
'actionAgainst': 'privilege',
'adverseActionId': '98765432-9876-9876-9876-987654321098',
'encumbranceType': 'suspension',
'clinicalPrivilegeActionCategories': ['Unsafe Practice or Substandard Care'],
'clinicalPrivilegeActionCategories': ['fraud'],
'compact': 'cosm',
'creationDate': '2024-11-08T23:59:59+00:00',
'effectiveStartDate': '2024-02-15',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def test_validate_patch_with_encumbrance(self):
'encumbrance': {
'encumbranceEffectiveDate': '2024-03-15',
'encumbranceType': 'suspension',
'clinicalPrivilegeActionCategories': ['Unsafe Practice or Substandard Care'],
'clinicalPrivilegeActionCategories': ['consumer harm'],
}
}
result = InvestigationPatchRequestSchema().load(investigation_data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@
)


def _ensure_jurisdiction_live(compact: str, jurisdiction: str) -> None:
live_jurisdictions = config.live_compact_jurisdictions.get(compact, [])
normalized = jurisdiction.lower()
if normalized not in {j.lower() for j in live_jurisdictions}:
raise CCInvalidRequestException('Jurisdiction is not live in this compact')


@api_handler
@authorize_state_level_only_action(action=CCPermissionsAction.ADMIN)
def encumbrance_handler(event: dict, context: LambdaContext) -> dict:
Expand Down Expand Up @@ -146,6 +153,7 @@ def handle_privilege_encumbrance(event: dict) -> dict:
# Parse event parameters
compact = event['pathParameters']['compact']
jurisdiction = event['pathParameters']['jurisdiction']
_ensure_jurisdiction_live(compact, jurisdiction)
provider_id = to_uuid(event['pathParameters']['providerId'], 'Invalid providerId provided')
license_type_abbr = event['pathParameters']['licenseType'].lower()
submitting_user = _get_submitting_user_id(event)
Expand Down Expand Up @@ -202,6 +210,7 @@ def handle_license_encumbrance(event: dict) -> dict:
# Parse event parameters
compact = event['pathParameters']['compact']
jurisdiction = event['pathParameters']['jurisdiction']
_ensure_jurisdiction_live(compact, jurisdiction)
provider_id = to_uuid(event['pathParameters']['providerId'], 'Invalid providerId provided')
license_type_abbr = event['pathParameters']['licenseType'].lower()
submitting_user = _get_submitting_user_id(event)
Expand Down Expand Up @@ -230,6 +239,7 @@ def handle_privilege_encumbrance_lifting(event: dict) -> dict:
compact = event['pathParameters']['compact']
provider_id = to_uuid(event['pathParameters']['providerId'], 'Invalid providerId provided')
jurisdiction = event['pathParameters']['jurisdiction']
_ensure_jurisdiction_live(compact, jurisdiction)
license_type_abbreviation = event['pathParameters']['licenseType'].lower()
encumbrance_id = to_uuid(event['pathParameters']['encumbranceId'], 'Invalid encumbranceId provided')

Expand Down Expand Up @@ -282,6 +292,7 @@ def handle_license_encumbrance_lifting(event: dict) -> dict:
compact = event['pathParameters']['compact']
provider_id = to_uuid(event['pathParameters']['providerId'], 'Invalid providerId provided')
jurisdiction = event['pathParameters']['jurisdiction']
_ensure_jurisdiction_live(compact, jurisdiction)
license_type_abbreviation = event['pathParameters']['licenseType'].lower()
encumbrance_id = to_uuid(event['pathParameters']['encumbranceId'], 'Invalid encumbranceId provided')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
DEFAULT_AA_SUBMITTING_USER_ID,
DEFAULT_COMPACT,
DEFAULT_DATE_OF_UPDATE_TIMESTAMP,
DEFAULT_ENCUMBRANCE_TYPE,
DEFAULT_LICENSE_JURISDICTION,
DEFAULT_LICENSE_TYPE,
DEFAULT_LICENSE_TYPE_ABBREVIATION,
Expand Down Expand Up @@ -50,7 +49,7 @@ def _generate_test_body():
'encumbranceEffectiveDate': TEST_ENCUMBRANCE_EFFECTIVE_DATE,
# These Enums are expected to be `str` type, so we'll directly access their .value
'encumbranceType': EncumbranceType.SUSPENSION.value,
'clinicalPrivilegeActionCategories': [ClinicalPrivilegeActionCategory.UNSAFE_PRACTICE.value],
'clinicalPrivilegeActionCategories': [ClinicalPrivilegeActionCategory.FRAUD.value],
}


Expand All @@ -59,6 +58,12 @@ def _generate_test_body():
class TestPostPrivilegeEncumbrance(TstFunction):
"""Test suite for privilege encumbrance endpoints."""

def setUp(self):
super().setUp()
self.set_live_compact_jurisdictions_for_test(
{'cosm': [DEFAULT_LICENSE_JURISDICTION, DEFAULT_PRIVILEGE_JURISDICTION]}
)

def _when_testing_privilege_encumbrance(self, body_overrides: dict | None = None):
self.test_data_generator.put_default_provider_record_in_provider_table()
self.test_data_generator.put_default_license_record_in_provider_table()
Expand Down Expand Up @@ -127,7 +132,6 @@ def test_privilege_encumbrance_handler_adds_adverse_action_record_in_provider_da
default_adverse_action_encumbrance = self.test_data_generator.generate_default_adverse_action(
value_overrides={
'adverseActionId': item['adverseActionId'],
'encumbranceType': DEFAULT_ENCUMBRANCE_TYPE,
'effectiveStartDate': date.fromisoformat(TEST_ENCUMBRANCE_EFFECTIVE_DATE),
'jurisdiction': DEFAULT_PRIVILEGE_JURISDICTION,
}
Expand Down Expand Up @@ -240,6 +244,12 @@ def test_privilege_encumbrance_handler_handles_event_publishing_failure(self, mo
class TestPostLicenseEncumbrance(TstFunction):
"""Test suite for license encumbrance endpoints."""

def setUp(self):
super().setUp()
self.set_live_compact_jurisdictions_for_test(
{'cosm': [DEFAULT_LICENSE_JURISDICTION, DEFAULT_PRIVILEGE_JURISDICTION]}
)

def _when_testing_valid_license_encumbrance(self, body_overrides: dict | None = None):
self.test_data_generator.put_default_provider_record_in_provider_table()
test_license_record = self.test_data_generator.put_default_license_record_in_provider_table()
Expand Down Expand Up @@ -308,7 +318,6 @@ def test_license_encumbrance_handler_adds_adverse_action_record_in_provider_data
value_overrides={
'actionAgainst': 'license',
'adverseActionId': item['adverseActionId'],
'encumbranceType': DEFAULT_ENCUMBRANCE_TYPE,
'effectiveStartDate': date.fromisoformat(TEST_ENCUMBRANCE_EFFECTIVE_DATE),
'jurisdiction': DEFAULT_LICENSE_JURISDICTION,
}
Expand Down Expand Up @@ -453,6 +462,12 @@ def test_license_encumbrance_handler_returns_400_if_encumbrance_date_in_future(s
class TestPatchPrivilegeEncumbranceLifting(TstFunction):
"""Test suite for privilege encumbrance lifting endpoints."""

def setUp(self):
super().setUp()
self.set_live_compact_jurisdictions_for_test(
{'cosm': [DEFAULT_LICENSE_JURISDICTION, DEFAULT_PRIVILEGE_JURISDICTION]}
)

def _setup_privilege_with_adverse_action(
self,
adverse_action_overrides=None,
Expand Down Expand Up @@ -766,6 +781,12 @@ def test_privilege_encumbrance_lifting_handler_handles_event_publishing_failure(
class TestPatchLicenseEncumbranceLifting(TstFunction):
"""Test suite for license encumbrance lifting endpoints."""

def setUp(self):
super().setUp()
self.set_live_compact_jurisdictions_for_test(
{'cosm': [DEFAULT_LICENSE_JURISDICTION, DEFAULT_PRIVILEGE_JURISDICTION]}
)

def _setup_license_with_adverse_action(self, adverse_action_overrides=None, license_overrides=None):
"""Helper method to set up a license with an adverse action for testing."""
self.test_data_generator.put_default_provider_record_in_provider_table(
Expand Down
Loading
Loading