Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b24ed8a
Remove deprecated 'clinicalPrivilegeActionCategory' from backend
landonshumway-ia Dec 30, 2025
3edc01f
Remove feature flag and test mocks
landonshumway-ia Dec 30, 2025
3086fae
formatting/linter
landonshumway-ia Dec 30, 2025
6f16956
Remove encumbrance feature flag from FE
landonshumway-ia Jan 5, 2026
639e801
set field as required
landonshumway-ia Jan 5, 2026
0c53cd8
update test snapshots with required field
landonshumway-ia Jan 5, 2026
b01e711
Remove old SK pattern when fetching privileges
landonshumway-ia Jan 5, 2026
4717529
remove migratory pre-load hook
landonshumway-ia Jan 5, 2026
144ca4a
add comment about encumbrance detail field
landonshumway-ia Jan 5, 2026
d05afd4
Update smoke tests to clean up privilege update records
landonshumway-ia Jan 5, 2026
d537e94
add notification email env var to smoke test
landonshumway-ia Jan 5, 2026
fbf166f
add waits for notification processing
landonshumway-ia Jan 5, 2026
3ec4b1d
formatting/linter
landonshumway-ia Jan 5, 2026
015ddb9
remove data migration stack
landonshumway-ia Jan 5, 2026
97dfb57
remove unused import
landonshumway-ia Jan 5, 2026
1515362
return npdb categories as list to privilege timeline
landonshumway-ia Jan 7, 2026
11b860f
update api response model for privilege history endpoint
landonshumway-ia Jan 7, 2026
bf9e83b
update test in common layer
landonshumway-ia Jan 7, 2026
520d809
set npdb categories to required in marshmallow schemas
landonshumway-ia Jan 7, 2026
1f3011e
display first npdb category from list when ending encumbrance
landonshumway-ia Jan 7, 2026
614c758
update api spec
landonshumway-ia Jan 7, 2026
45cf834
PR feedback - using the encumbrance type name instead of npdb category
landonshumway-ia Jan 8, 2026
8c34f76
PR feedback - fix comment
landonshumway-ia Jan 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
16,326 changes: 9,874 additions & 6,452 deletions backend/compact-connect/docs/internal/api-specification/latest-oas30.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -1021,9 +1021,6 @@ def _get_privilege_update_records_directly(
This should be used when it is undesirable to get all provider update records and
filter for the specific privilege update records.

During migration period, this method queries both the new and old SK patterns to ensure
no records are missed.

:param str compact: The compact of the privilege
:param str provider_id: The provider of the privilege
:param str jurisdiction: The jurisdiction of the privilege
Expand All @@ -1032,36 +1029,27 @@ def _get_privilege_update_records_directly(
:return: List of privilege update records
"""
pk = f'{compact}#PROVIDER#{provider_id}'

# SK prefixes to query (new pattern and old pattern for migration support)
# TODO - remove old pattern once migration is complete # noqa: FIX002
sk_prefixes = [
# New pattern
f'{compact}#UPDATE#{UpdateTierEnum.TIER_ONE}#privilege/{jurisdiction}/{license_type_abbr}/',
# Old pattern
f'{compact}#PROVIDER#privilege/{jurisdiction}/{license_type_abbr}#UPDATE',
]
sk_prefix = f'{compact}#UPDATE#{UpdateTierEnum.TIER_ONE}#privilege/{jurisdiction}/{license_type_abbr}/'

response_items = []

# Query for records using each SK prefix pattern
for sk_prefix in sk_prefixes:
last_evaluated_key = None
while True:
pagination = {'ExclusiveStartKey': last_evaluated_key} if last_evaluated_key else {}

query_resp = self.config.provider_table.query(
Select='ALL_ATTRIBUTES',
KeyConditionExpression=Key('pk').eq(pk) & Key('sk').begins_with(sk_prefix),
ConsistentRead=consistent_read,
**pagination,
)
# Query for records using the SK prefix pattern
last_evaluated_key = None
while True:
pagination = {'ExclusiveStartKey': last_evaluated_key} if last_evaluated_key else {}

query_resp = self.config.provider_table.query(
Select='ALL_ATTRIBUTES',
KeyConditionExpression=Key('pk').eq(pk) & Key('sk').begins_with(sk_prefix),
ConsistentRead=consistent_read,
**pagination,
)

response_items.extend(query_resp.get('Items', []))
response_items.extend(query_resp.get('Items', []))

last_evaluated_key = query_resp.get('LastEvaluatedKey')
if not last_evaluated_key:
break
last_evaluated_key = query_resp.get('LastEvaluatedKey')
if not last_evaluated_key:
break

return [PrivilegeUpdateData.from_database_record(item) for item in response_items]

Expand Down Expand Up @@ -1527,19 +1515,10 @@ def encumber_privilege(self, adverse_action: AdverseActionData) -> None:
)

now = config.current_standard_datetime
# TODO - remove the flag as part of https://github.com/csg-org/CompactConnect/issues/1136 # noqa: FIX002
from cc_common.feature_flag_client import FeatureFlagEnum, is_feature_enabled

if is_feature_enabled(FeatureFlagEnum.ENCUMBRANCE_MULTI_CATEGORY_FLAG):
encumbrance_details = {
'clinicalPrivilegeActionCategories': adverse_action.clinicalPrivilegeActionCategories,
'adverseActionId': adverse_action.adverseActionId,
}
else:
encumbrance_details = {
'clinicalPrivilegeActionCategory': adverse_action.clinicalPrivilegeActionCategory,
'adverseActionId': adverse_action.adverseActionId,
}
encumbrance_details = {
'clinicalPrivilegeActionCategories': adverse_action.clinicalPrivilegeActionCategories,
'adverseActionId': adverse_action.adverseActionId,
}
Comment thread
landonshumway-ia marked this conversation as resolved.

# The time selected here is somewhat arbitrary; however, we want this selection to not alter the date
# displayed for a user when it is transformed back to their timezone. We selected noon UTC-4:00 so that
Expand Down Expand Up @@ -3070,21 +3049,13 @@ def encumber_home_jurisdiction_license_privileges(
logger.info(
'Found privileges to encumber', privilege_count=len(unencumbered_privileges_associated_with_license)
)
# TODO - remove the flag as part of https://github.com/csg-org/CompactConnect/issues/1136 # noqa: FIX002
from cc_common.feature_flag_client import FeatureFlagEnum, is_feature_enabled

if is_feature_enabled(FeatureFlagEnum.ENCUMBRANCE_MULTI_CATEGORY_FLAG):
encumbrance_details = {
'clinicalPrivilegeActionCategories': adverse_action.clinicalPrivilegeActionCategories,
'licenseJurisdiction': jurisdiction,
'adverseActionId': adverse_action_id,
}
else:
encumbrance_details = {
'clinicalPrivilegeActionCategory': adverse_action.clinicalPrivilegeActionCategory,
'licenseJurisdiction': jurisdiction,
'adverseActionId': adverse_action_id,
}
encumbrance_details = {
'clinicalPrivilegeActionCategories': adverse_action.clinicalPrivilegeActionCategories,
# In the case of privileges being encumbered due to the home state license being encumbered,
# this 'licenseJurisdiction' field is added to denote which license was responsible for this update.
'licenseJurisdiction': jurisdiction,
'adverseActionId': adverse_action_id,
}

# Build transaction items for all privileges
transaction_items = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,18 +383,8 @@ def construct_simplified_privilege_history_object(
and event.get('encumbranceDetails')
and should_include_encumbrance_details
):
# TODO - remove the flag as part of https://github.com/csg-org/CompactConnect/issues/1136 # noqa: FIX002
# as well as check for deprecated field
from cc_common.feature_flag_client import FeatureFlagEnum, is_feature_enabled

if is_feature_enabled(FeatureFlagEnum.ENCUMBRANCE_MULTI_CATEGORY_FLAG):
if 'clinicalPrivilegeActionCategory' in event['encumbranceDetails']:
event['note'] = event['encumbranceDetails'].get('clinicalPrivilegeActionCategory')
else:
# else we are using the new field, parse the list into a comma-separated string
event['note'] = ', '.join(event['encumbranceDetails']['clinicalPrivilegeActionCategories'])
else:
event['note'] = event['encumbranceDetails']['clinicalPrivilegeActionCategory']
# In the case of encumbrances, we return the list of npdb categories associated with it
event['npdbCategories'] = event['encumbranceDetails']['clinicalPrivilegeActionCategories']
Comment thread
landonshumway-ia marked this conversation as resolved.
elif event['updateType'] == UpdateCategory.DEACTIVATION and event.get('deactivationDetails'):
event['note'] = event['deactivationDetails']['note']

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from cc_common.data_model.schema.common import (
AdverseActionAgainstEnum,
CCDataClass,
ClinicalPrivilegeActionCategory,
EncumbranceType,
)

Expand Down Expand Up @@ -79,17 +78,6 @@ def encumbranceType(self) -> str:
def encumbranceType(self, encumbrance_type_enum: EncumbranceType) -> None:
self._data['encumbranceType'] = encumbrance_type_enum.value

# TODO - remove deprecated getter/setter after migrating to 'clinicalPrivilegeActionCategories' field # noqa: FIX002
@property
def clinicalPrivilegeActionCategory(self) -> str | None:
return self._data.get('clinicalPrivilegeActionCategory')

@clinicalPrivilegeActionCategory.setter
def clinicalPrivilegeActionCategory(
self, clinical_privilege_action_category_enum: ClinicalPrivilegeActionCategory
) -> None:
self._data['clinicalPrivilegeActionCategory'] = clinical_privilege_action_category_enum.value

@property
def clinicalPrivilegeActionCategories(self) -> list[str] | None:
return self._data.get('clinicalPrivilegeActionCategories')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# ruff: noqa: N801, N815 invalid-name
from marshmallow import ValidationError, validates_schema
from marshmallow.fields import Date, List, Raw, String
from marshmallow.validate import Length, OneOf

Expand All @@ -26,27 +25,8 @@ class AdverseActionPostRequestSchema(ForgivingSchema):
encumbranceEffectiveDate = Date(required=True, allow_none=False)
encumbranceType = EncumbranceTypeField(required=True, allow_none=False)
clinicalPrivilegeActionCategories = List(
ClinicalPrivilegeActionCategoryField(), required=False, allow_none=False, validate=Length(min=1)
ClinicalPrivilegeActionCategoryField(), required=True, allow_none=False, validate=Length(min=1)
)
# TODO - remove this field as part of https://github.com/csg-org/CompactConnect/issues/1136 # noqa: FIX002
clinicalPrivilegeActionCategory = ClinicalPrivilegeActionCategoryField(required=False, allow_none=False)

@validates_schema
def validate_clinical_privilege_action_category_fields(self, data, **_kwargs):
"""Ensure exactly one of the category fields is provided."""
has_singular = 'clinicalPrivilegeActionCategory' in data
has_plural = 'clinicalPrivilegeActionCategories' in data

if has_singular and has_plural:
raise ValidationError(
'Cannot provide both clinicalPrivilegeActionCategory and clinicalPrivilegeActionCategories. '
'Use clinicalPrivilegeActionCategories (the singular field is deprecated).'
)

if not has_singular and not has_plural:
raise ValidationError(
'Must provide either clinicalPrivilegeActionCategory or clinicalPrivilegeActionCategories.'
)


class AdverseActionPatchRequestSchema(ForgivingSchema):
Expand Down Expand Up @@ -101,5 +81,3 @@ class AdverseActionGeneralResponseSchema(AdverseActionPublicResponseSchema):
clinicalPrivilegeActionCategories = List(ClinicalPrivilegeActionCategoryField(), required=False, allow_none=False)
liftingUser = Raw(required=False, allow_none=False)
submittingUser = Raw(required=True, allow_none=False)
# TODO - remove this field as part of https://github.com/csg-org/CompactConnect/issues/1136 # noqa: FIX002
clinicalPrivilegeActionCategory = ClinicalPrivilegeActionCategoryField(required=False, allow_none=False)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ruff: noqa: N801, N815 invalid-name
from marshmallow import ValidationError, pre_dump, pre_load, validates_schema
from marshmallow import ValidationError, pre_dump, validates_schema
from marshmallow.fields import UUID, Date, DateTime, List, String
from marshmallow.validate import OneOf

Expand Down Expand Up @@ -34,35 +34,20 @@ class AdverseActionRecordSchema(BaseRecordSchema):

# Populated on creation
encumbranceType = EncumbranceTypeField(required=True, allow_none=False)
clinicalPrivilegeActionCategories = List(ClinicalPrivilegeActionCategoryField(), required=False, allow_none=False)
clinicalPrivilegeActionCategories = List(ClinicalPrivilegeActionCategoryField(), required=True, allow_none=False)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
effectiveStartDate = Date(required=True, allow_none=False)
submittingUser = UUID(required=True, allow_none=False)
creationDate = DateTime(required=True, allow_none=False)
adverseActionId = UUID(required=True, allow_none=False)
# TODO - remove this field as part of https://github.com/csg-org/CompactConnect/issues/1136 # noqa: FIX002
clinicalPrivilegeActionCategory = ClinicalPrivilegeActionCategoryField(required=False, allow_none=False)

# Populated when the action is lifted
effectiveLiftDate = Date(required=False, allow_none=False)
liftingUser = UUID(required=False, allow_none=False)

# TODO - remove this hook once migration is complete to the new field # noqa: FIX002
@pre_load
def migrate_clinical_privilege_action_category_on_load(self, in_data, **_kwargs):
"""Migrate deprecated clinicalPrivilegeActionCategory to clinicalPrivilegeActionCategories list
when loading from database."""
# If the deprecated field exists and the new field doesn't, migrate it
if 'clinicalPrivilegeActionCategory' in in_data and 'clinicalPrivilegeActionCategories' not in in_data:
in_data['clinicalPrivilegeActionCategories'] = [in_data['clinicalPrivilegeActionCategory']]
# Remove the deprecated field to avoid having both
del in_data['clinicalPrivilegeActionCategory']
return in_data

@pre_dump
def pre_dump_serialization(self, in_data, **_kwargs):
"""Pre-dump serialization to ensure the clinicalPrivilegeActionCategories list is serialized correctly."""
in_data = self.generate_pk_sk(in_data)
return self.migrate_clinical_privilege_action_category(in_data)
return self.generate_pk_sk(in_data)

def generate_pk_sk(self, in_data, **_kwargs):
in_data['pk'] = f'{in_data["compact"]}#PROVIDER#{in_data["providerId"]}'
Expand All @@ -73,14 +58,6 @@ def generate_pk_sk(self, in_data, **_kwargs):
)
return in_data

# TODO - remove this hook as part of https://github.com/csg-org/CompactConnect/issues/1136 # noqa: FIX002
def migrate_clinical_privilege_action_category(self, in_data, **_kwargs):
"""Migrate deprecated clinicalPrivilegeActionCategory to clinicalPrivilegeActionCategories list."""
# If the deprecated field exists and the new field doesn't, migrate it for backwards compatibility
if 'clinicalPrivilegeActionCategory' in in_data and 'clinicalPrivilegeActionCategories' not in in_data:
in_data['clinicalPrivilegeActionCategories'] = [in_data['clinicalPrivilegeActionCategory']]
return in_data

@validates_schema
def validate_license_type(self, data, **_kwargs): # noqa: ARG001 unused-argument
compact = data['compact']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ class PrivilegeHistoryEventResponseSchema(ForgivingSchema):
effectiveDate = Raw(required=True, allow_none=False)
createDate = Raw(required=True, allow_none=False)
note = String(required=False, allow_none=True)
# in the case of encumbrance events, we return the list of categories rather than a note
npdbCategories = List(String(), required=False, allow_none=True)


class PrivilegeHistoryResponseSchema(ForgivingSchema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,10 @@ class EncumbranceDetailsSchema(Schema):
Schema for tracking details about an encumbrance.
"""

clinicalPrivilegeActionCategories = List(ClinicalPrivilegeActionCategoryField(), required=False, allow_none=False)
clinicalPrivilegeActionCategories = List(ClinicalPrivilegeActionCategoryField(), required=True, allow_none=False)
adverseActionId = UUID(required=True, allow_none=False)
# present if update is created by upstream license encumbrance
licenseJurisdiction = Jurisdiction(required=False, allow_none=False)
# TODO - remove this field as part of https://github.com/csg-org/CompactConnect/issues/1136 # noqa: FIX002
clinicalPrivilegeActionCategory = ClinicalPrivilegeActionCategoryField(required=False, allow_none=False)


@BaseRecordSchema.register_schema('privilege')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,17 +233,6 @@ class ProviderUpdateRecordSchema(BaseRecordSchema, ChangeHashMixin):
# List of field names that were present in the previous record but removed in the update
removedValues = List(String(), required=False, allow_none=False)

# TODO - remove this pre_load hook after migration is complete # noqa: FIX002
@pre_load
def populate_create_date_for_backwards_compatibility(self, in_data, **kwargs): # noqa: ARG001 unused-argument
"""
For backwards compatibility, populate createDate from dateOfUpdate if createDate is missing.
This allows us to load old records that were created before the createDate field was added.
"""
if 'createDate' not in in_data:
in_data['createDate'] = in_data['dateOfUpdate']
return in_data

@post_dump # Must be _post_ dump so we have values that are more easily hashed
def generate_pk_sk(self, in_data, **kwargs): # noqa: ARG001 unused-argument
in_data['pk'] = f'{in_data["compact"]}#PROVIDER#{in_data["providerId"]}'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,4 @@ class FeatureFlagEnum(StrEnum):
# flag used by internal testing
TEST_FLAG = 'test-flag'
# runtime flags
ENCUMBRANCE_MULTI_CATEGORY_FLAG = 'encumbrance-multi-category-flag'
DUPLICATE_SSN_UPLOAD_CHECK_FLAG = 'duplicate-ssn-upload-check-flag'
Loading
Loading