From 02c712c075bcab4add6bafd92d493f0c19b54643 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Apr 2026 16:09:55 -0500 Subject: [PATCH 01/15] Setting 'providerDateOfUpdate' field on update expressions In order for states to programmatically poll for providers that have privileges within their state through the State 'two-way' API, the system relies on a 'providerDateOfUpdate' field to be updated on the provider record whenever a change occurs on their record. Currently, when a privilege is purchased, we are updating several fields on the provider record, but not the 'providerDateOfUpdate' field, resulting in providers not being returned in the API when states go to query for providers, unless they have another update take place from somewhere else in the system. A state reported an issue where a provider purchased privileges in their state on 3/15, but because that individual has not had any other updates to their records, their 'providerDateOfUpdate' field is set to 02/26 before the privilege was purchased. Which means that the expected provider is not returned from the 'two-way' API within the expected time range. This updates all the locations where we are performing updates that impact the privilege status so that we also update the 'providerDateOfUpdate' field on the top-level provider record so that the provider will be returned in the 'two-way' API when states query for provider updates. --- .../cc_common/data_model/data_client.py | 53 ++++++-- .../function/test_home_jurisdiction_events.py | 28 +++++ .../test_handlers/test_encumbrance.py | 118 ++++++++++++++++-- .../function/test_handlers/test_privileges.py | 36 ++++++ .../test_handlers/test_provider_users.py | 25 ++++ .../test_handlers/test_purchase_privileges.py | 25 ++++ 6 files changed, 269 insertions(+), 16 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py index 19be0ec21..c0092d299 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py @@ -527,9 +527,14 @@ def create_provider_privileges( 'pk': {'S': f'{compact}#PROVIDER#{provider_id}'}, 'sk': {'S': f'{compact}#PROVIDER'}, }, - 'UpdateExpression': 'ADD #privilegeJurisdictions :newJurisdictions', + 'UpdateExpression': 'ADD #privilegeJurisdictions :newJurisdictions ' + 'SET dateOfUpdate = :dateOfUpdate, providerDateOfUpdate = :providerDateOfUpdate', 'ExpressionAttributeNames': {'#privilegeJurisdictions': 'privilegeJurisdictions'}, - 'ExpressionAttributeValues': {':newJurisdictions': {'SS': jurisdiction_postal_abbreviations}}, + 'ExpressionAttributeValues': { + ':newJurisdictions': {'SS': jurisdiction_postal_abbreviations}, + ':dateOfUpdate': {'S': now.isoformat()}, + ':providerDateOfUpdate': {'S': now.isoformat()}, + }, } } ) @@ -902,9 +907,13 @@ def end_military_affiliation(self, compact: str, provider_id: str) -> None: 'pk': {'S': provider_serialized_record['pk']}, 'sk': {'S': provider_serialized_record['sk']}, }, - 'UpdateExpression': ('SET dateOfUpdate = :dateOfUpdate REMOVE militaryStatus, militaryStatusNote'), + 'UpdateExpression': ( + 'SET dateOfUpdate = :dateOfUpdate, providerDateOfUpdate = :providerDateOfUpdate ' + 'REMOVE militaryStatus, militaryStatusNote' + ), 'ExpressionAttributeValues': { ':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + ':providerDateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, }, 'ConditionExpression': 'attribute_exists(pk)', } @@ -1477,6 +1486,22 @@ def deactivate_privilege( }, }, }, + # Update dateOfUpdate and providerDateOfUpdate on the top-level provider record + { + 'Update': { + 'TableName': self.config.provider_table.name, + 'Key': { + 'pk': {'S': f'{compact}#PROVIDER#{provider_id}'}, + 'sk': {'S': f'{compact}#PROVIDER'}, + }, + 'UpdateExpression': 'SET dateOfUpdate = :dateOfUpdate, ' + 'providerDateOfUpdate = :providerDateOfUpdate', + 'ExpressionAttributeValues': { + ':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + ':providerDateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + }, + }, + }, # Create a history record, reflecting this change { 'Put': { @@ -1560,10 +1585,20 @@ def _generate_set_provider_encumbered_status_item( # licenses and providers share the same encumbered status enum provider_encumbered_status: LicenseEncumberedStatusEnum, ): - return self._generate_encumbered_status_update_item( - data=provider_data, - encumbered_status=provider_encumbered_status, - ) + data_record = provider_data.serialize_to_database_record() + return { + 'Update': { + 'TableName': self.config.provider_table.name, + 'Key': {'pk': {'S': data_record['pk']}, 'sk': {'S': data_record['sk']}}, + 'UpdateExpression': 'SET encumberedStatus = :status, dateOfUpdate = :dateOfUpdate, ' + 'providerDateOfUpdate = :providerDateOfUpdate', + 'ExpressionAttributeValues': { + ':status': {'S': provider_encumbered_status}, + ':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + ':providerDateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + }, + }, + } def _generate_put_transaction_item(self, item: dict): return {'Put': {'TableName': self.config.provider_table.name, 'Item': TypeSerializer().serialize(item)['M']}} @@ -2965,10 +3000,12 @@ def _get_provider_record_transaction_items_for_jurisdiction_with_no_known_licens }, 'UpdateExpression': 'SET ' 'currentHomeJurisdiction = :currentHomeJurisdiction, ' - 'dateOfUpdate = :dateOfUpdate', + 'dateOfUpdate = :dateOfUpdate, ' + 'providerDateOfUpdate = :providerDateOfUpdate', 'ExpressionAttributeValues': { ':currentHomeJurisdiction': {'S': selected_jurisdiction}, ':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + ':providerDateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, }, } }, diff --git a/backend/compact-connect/lambdas/python/data-events/tests/function/test_home_jurisdiction_events.py b/backend/compact-connect/lambdas/python/data-events/tests/function/test_home_jurisdiction_events.py index f314af838..3aa165c65 100644 --- a/backend/compact-connect/lambdas/python/data-events/tests/function/test_home_jurisdiction_events.py +++ b/backend/compact-connect/lambdas/python/data-events/tests/function/test_home_jurisdiction_events.py @@ -341,3 +341,31 @@ def test_no_notification_when_jurisdiction_config_not_found(self, mock_send_new_ # Verify no emails were sent mock_send_old_state.assert_not_called() mock_send_new_state.assert_not_called() + + def test_provider_date_of_update_is_set_after_home_jurisdiction_change(self): + """Test that providerDateOfUpdate is updated on the provider record after a home jurisdiction change + to a jurisdiction with no known license (Update expression path).""" + from cc_common.config import config + + self.test_data_generator.put_default_provider_record_in_provider_table( + value_overrides={ + 'currentHomeJurisdiction': 'oh', + }, + date_of_update_override='2020-01-01T00:00:00+00:00', + ) + + config.data_client.update_provider_home_state_jurisdiction( + compact=DEFAULT_COMPACT, + provider_id=DEFAULT_PROVIDER_ID, + selected_jurisdiction='tx', + ) + + provider_record = self._provider_table.get_item( + Key={ + 'pk': f'{DEFAULT_COMPACT}#PROVIDER#{DEFAULT_PROVIDER_ID}', + 'sk': f'{DEFAULT_COMPACT}#PROVIDER', + } + )['Item'] + + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['dateOfUpdate']) + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['providerDateOfUpdate']) diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py index 9179e758d..72bbd97a5 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py +++ b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py @@ -56,8 +56,12 @@ def _generate_test_body(): class TestPostPrivilegeEncumbrance(TstFunction): """Test suite for privilege encumbrance endpoints.""" - def _when_testing_privilege_encumbrance(self, body_overrides: dict | None = None): - self.test_data_generator.put_default_provider_record_in_provider_table() + def _when_testing_privilege_encumbrance( + self, body_overrides: dict | None = None, date_of_update_override: str | None = None + ): + self.test_data_generator.put_default_provider_record_in_provider_table( + date_of_update_override=date_of_update_override, + ) test_privilege_record = self.test_data_generator.put_default_privilege_record_in_provider_table() body = _generate_test_body() @@ -300,14 +304,39 @@ def test_privilege_encumbrance_handler_handles_event_publishing_failure(self, mo encumbrance_handler(event, self.mock_context) self.assertEqual('Event publishing failed', str(context.exception)) + def test_privilege_encumbrance_updates_provider_date_of_update(self): + """Test that encumbering a privilege updates providerDateOfUpdate on the provider record.""" + from handlers.encumbrance import encumbrance_handler + + event, test_privilege_record = self._when_testing_privilege_encumbrance( + date_of_update_override='2020-01-01T00:00:00+00:00', + ) + + response = encumbrance_handler(event, self.mock_context) + self.assertEqual(200, response['statusCode'], msg=json.loads(response['body'])) + + provider_record = self._provider_table.get_item( + Key={ + 'pk': f'{test_privilege_record.compact}#PROVIDER#{test_privilege_record.providerId}', + 'sk': f'{test_privilege_record.compact}#PROVIDER', + } + )['Item'] + + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['dateOfUpdate']) + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['providerDateOfUpdate']) + @mock_aws @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat(DEFAULT_DATE_OF_UPDATE_TIMESTAMP)) class TestPostLicenseEncumbrance(TstFunction): """Test suite for license encumbrance endpoints.""" - def _when_testing_valid_license_encumbrance(self, body_overrides: dict | None = None): - self.test_data_generator.put_default_provider_record_in_provider_table() + def _when_testing_valid_license_encumbrance( + self, body_overrides: dict | None = None, date_of_update_override: str | None = None + ): + self.test_data_generator.put_default_provider_record_in_provider_table( + date_of_update_override=date_of_update_override, + ) test_license_record = self.test_data_generator.put_default_license_record_in_provider_table() body = _generate_test_body() @@ -513,6 +542,27 @@ def test_license_encumbrance_handler_returns_400_if_encumbrance_date_in_future(s response_body, ) + def test_license_encumbrance_updates_provider_date_of_update(self): + """Test that encumbering a license updates providerDateOfUpdate on the provider record.""" + from handlers.encumbrance import encumbrance_handler + + event, test_license_record = self._when_testing_valid_license_encumbrance( + date_of_update_override='2020-01-01T00:00:00+00:00', + ) + + response = encumbrance_handler(event, self.mock_context) + self.assertEqual(200, response['statusCode'], msg=json.loads(response['body'])) + + provider_record = self._provider_table.get_item( + Key={ + 'pk': f'{test_license_record.compact}#PROVIDER#{test_license_record.providerId}', + 'sk': f'{test_license_record.compact}#PROVIDER', + } + )['Item'] + + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['dateOfUpdate']) + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['providerDateOfUpdate']) + @mock_aws @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat(DEFAULT_DATE_OF_UPDATE_TIMESTAMP)) @@ -520,11 +570,16 @@ class TestPatchPrivilegeEncumbranceLifting(TstFunction): """Test suite for privilege encumbrance lifting endpoints.""" def _setup_privilege_with_adverse_action( - self, adverse_action_overrides=None, privilege_overrides=None, license_overrides=None + self, + adverse_action_overrides=None, + privilege_overrides=None, + license_overrides=None, + date_of_update_override: str | None = None, ): """Helper method to set up a privilege with an adverse action for testing.""" self.test_data_generator.put_default_provider_record_in_provider_table( - value_overrides={'encumberedStatus': 'encumbered'} + value_overrides={'encumberedStatus': 'encumbered'}, + date_of_update_override=date_of_update_override, ) test_privilege_record = self.test_data_generator.put_default_privilege_record_in_provider_table( value_overrides=privilege_overrides or {'encumberedStatus': 'encumbered'} @@ -892,16 +947,41 @@ def test_privilege_encumbrance_lifting_handler_handles_event_publishing_failure( encumbrance_handler(event, self.mock_context) self.assertEqual('Event publishing failed', str(context.exception)) + def test_should_update_provider_date_of_update_when_last_privilege_encumbrance_is_lifted(self): + """Test that lifting the last privilege encumbrance updates providerDateOfUpdate on the provider record.""" + from handlers.encumbrance import encumbrance_handler + + privilege_record, adverse_action = self._setup_privilege_with_adverse_action( + date_of_update_override='2020-01-01T00:00:00+00:00', + ) + event = self._generate_lift_encumbrance_event(privilege_record, adverse_action) + + response = encumbrance_handler(event, self.mock_context) + self.assertEqual(200, response['statusCode']) + + provider_record = self._provider_table.get_item( + Key={ + 'pk': f'{privilege_record.compact}#PROVIDER#{privilege_record.providerId}', + 'sk': f'{privilege_record.compact}#PROVIDER', + } + )['Item'] + + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['dateOfUpdate']) + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['providerDateOfUpdate']) + @mock_aws @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat(DEFAULT_DATE_OF_UPDATE_TIMESTAMP)) class TestPatchLicenseEncumbranceLifting(TstFunction): """Test suite for license encumbrance lifting endpoints.""" - def _setup_license_with_adverse_action(self, adverse_action_overrides=None, license_overrides=None): + def _setup_license_with_adverse_action( + self, adverse_action_overrides=None, license_overrides=None, date_of_update_override: str | None = 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( - value_overrides={'encumberedStatus': 'encumbered'} + value_overrides={'encumberedStatus': 'encumbered'}, + date_of_update_override=date_of_update_override, ) test_license_record = self.test_data_generator.put_default_license_record_in_provider_table( value_overrides=license_overrides or {'encumberedStatus': 'encumbered'} @@ -1177,6 +1257,28 @@ def test_should_not_update_provider_record_when_encumbered_privilege_exists(self loaded_provider_data = provider_records.get_provider_record() self.assertEqual(LicenseEncumberedStatusEnum.ENCUMBERED, loaded_provider_data.encumberedStatus) + def test_should_update_provider_date_of_update_when_last_license_encumbrance_is_lifted(self): + """Test that lifting the last license encumbrance updates providerDateOfUpdate on the provider record.""" + from handlers.encumbrance import encumbrance_handler + + license_record, adverse_action = self._setup_license_with_adverse_action( + date_of_update_override='2020-01-01T00:00:00+00:00', + ) + event = self._generate_lift_encumbrance_event(license_record, adverse_action) + + response = encumbrance_handler(event, self.mock_context) + self.assertEqual(200, response['statusCode']) + + provider_record = self._provider_table.get_item( + Key={ + 'pk': f'{license_record.compact}#PROVIDER#{license_record.providerId}', + 'sk': f'{license_record.compact}#PROVIDER', + } + )['Item'] + + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['dateOfUpdate']) + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['providerDateOfUpdate']) + def test_should_return_access_denied_if_compact_admin_attempts_to_lift_license_encumbrance(self): """Verifying that only state admins are allowed to lift license encumbrances""" from handlers.encumbrance import encumbrance_handler diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py index d71ecd192..cb45d0462 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py +++ b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py @@ -95,6 +95,8 @@ def _assert_the_privilege_was_deactivated(self): expected_provider['privileges'][0]['dateOfUpdate'] = '2024-11-08T23:59:59+00:00' # remove activeSince Field, since the privilege in this case would not be active del expected_provider['privileges'][0]['activeSince'] + # deactivating a privilege now also updates the provider record's dateOfUpdate + expected_provider['dateOfUpdate'] = '2024-11-08T23:59:59+00:00' body = json.loads(resp['body']) self.assertEqual(expected_provider, body) @@ -301,3 +303,37 @@ def test_privilege_purchase_message_handler_sends_email(self): privilege_purchase_message_handler(purchase_event, self.mock_context) mock_email_service_client.send_privilege_purchase_email.assert_called_once() + + @patch('cc_common.config._Config.email_service_client') + def test_deactivate_privilege_updates_provider_date_of_update(self, mock_email_service_client): # noqa: ARG002 unused-argument + """Test that deactivating a privilege updates dateOfUpdate and providerDateOfUpdate on the provider record.""" + from common_test.test_constants import DEFAULT_DATE_OF_UPDATE_TIMESTAMP, DEFAULT_PROVIDER_ID + from handlers.privileges import deactivate_privilege + + self._load_provider_data() + + with open('../common/tests/resources/api-event.json') as f: + event = json.load(f) + + event['requestContext']['authorizer']['claims']['scope'] = 'openid email aslp/admin' + event['requestContext']['authorizer']['claims']['sub'] = TEST_STAFF_USER_ID + event['pathParameters'] = { + 'compact': 'aslp', + 'providerId': DEFAULT_PROVIDER_ID, + 'jurisdiction': 'ne', + 'licenseType': 'slp', + } + event['body'] = json.dumps({'deactivationNote': TEST_NOTE}) + + resp = deactivate_privilege(event, self.mock_context) + self.assertEqual(200, resp['statusCode']) + + provider_record = self._provider_table.get_item( + Key={ + 'pk': f'aslp#PROVIDER#{DEFAULT_PROVIDER_ID}', + 'sk': 'aslp#PROVIDER', + } + )['Item'] + + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['dateOfUpdate']) + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['providerDateOfUpdate']) diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users.py b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users.py index 978d28c37..e4c0874d9 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users.py +++ b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users.py @@ -495,3 +495,28 @@ def test_patch_provider_military_affiliation_creates_provider_update_record(self self.assertEqual(['militaryStatus', 'militaryStatusNote'], update_data.removedValues) # Verify updated values is empty (no fields were updated, only removed) self.assertEqual({}, update_data.updatedValues) + + def test_patch_provider_military_affiliation_updates_provider_date_of_update(self): + """Test that ending military affiliation updates providerDateOfUpdate on the provider record.""" + from handlers.provider_users import provider_users_api_handler + + test_provider = self.test_data_generator.put_default_provider_record_in_provider_table( + value_overrides={'militaryStatus': 'declined'}, + date_of_update_override='2020-01-01T00:00:00+00:00', + ) + self.test_data_generator.put_default_military_affiliation_in_provider_table() + + event = self._generate_path_api_event(test_provider) + + resp = provider_users_api_handler(event, self.mock_context) + self.assertEqual(200, resp['statusCode']) + + provider_record = self._provider_table.get_item( + Key={ + 'pk': f'{TEST_COMPACT}#PROVIDER#{test_provider.providerId}', + 'sk': f'{TEST_COMPACT}#PROVIDER', + } + )['Item'] + + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['dateOfUpdate']) + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_record['providerDateOfUpdate']) diff --git a/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py b/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py index c3d88f569..28cc8a9de 100644 --- a/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py +++ b/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py @@ -1245,3 +1245,28 @@ def test_post_purchase_privileges_stores_unsettled_transaction(self, mock_purcha expected_epoch = int(dt.timestamp()) expected_sk = f'COMPACT#{TEST_COMPACT}#TIME#{expected_epoch}#TX#{MOCK_TRANSACTION_ID}' self.assertEqual(expected_sk, unsettled_tx['sk']) + + @patch('handlers.privileges.PurchaseClient') + def test_post_purchase_privileges_updates_provider_date_of_update(self, mock_purchase_client_constructor): + """Test that purchasing privileges updates dateOfUpdate and providerDateOfUpdate on the provider record.""" + from handlers.privileges import post_purchase_privileges + + self._when_purchase_client_successfully_processes_request(mock_purchase_client_constructor) + + event = self._when_testing_provider_user_event_with_custom_claims(license_expiration_date='2050-01-01') + event['body'] = _generate_test_request_body() + + resp = post_purchase_privileges(event, self.mock_context) + self.assertEqual(200, resp['statusCode'], resp['body']) + + expected_datetime = '2024-11-08T23:59:59+00:00' + + provider_record = self._provider_table.get_item( + Key={ + 'pk': f'{TEST_COMPACT}#PROVIDER#{TEST_PROVIDER_ID}', + 'sk': f'{TEST_COMPACT}#PROVIDER', + } + )['Item'] + + self.assertEqual(expected_datetime, provider_record['dateOfUpdate']) + self.assertEqual(expected_datetime, provider_record['providerDateOfUpdate']) From 5913febdc2bc4c9d08b84d324468d799b3343a21 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Apr 2026 16:27:05 -0500 Subject: [PATCH 02/15] Update python requirements --- .../cognito-backup/requirements-dev.txt | 26 +++---- .../lambdas/python/common/requirements-dev.in | 2 + .../python/common/requirements-dev.txt | 72 +++++++++++-------- .../lambdas/python/common/requirements.txt | 14 ++-- .../requirements-dev.txt | 20 +++--- .../custom-resources/requirements-dev.txt | 20 +++--- .../python/data-events/requirements-dev.txt | 20 +++--- .../disaster-recovery/requirements-dev.txt | 20 +++--- .../provider-data-v1/requirements-dev.txt | 22 +++--- .../python/search/requirements-dev.txt | 20 +++--- .../lambdas/python/search/requirements.txt | 10 +-- .../staff-user-pre-token/requirements-dev.txt | 20 +++--- .../python/staff-users/requirements-dev.txt | 24 +++---- backend/compact-connect/requirements-dev.txt | 32 ++++----- backend/compact-connect/requirements.txt | 14 ++-- 15 files changed, 174 insertions(+), 162 deletions(-) diff --git a/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt b/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt index 8690117bf..478d9a5ca 100644 --- a/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt @@ -4,25 +4,25 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/cognito-backup/requirements-dev.in # -aws-lambda-powertools==3.24.0 +aws-lambda-powertools==3.27.0 # via -r lambdas/python/cognito-backup/requirements-dev.in -boto3==1.42.42 +boto3==1.42.84 # via # -r lambdas/python/cognito-backup/requirements-dev.in # moto -botocore==1.42.42 +botocore==1.42.84 # via # -r lambdas/python/cognito-backup/requirements-dev.in # boto3 # moto # s3transfer -certifi==2026.1.4 +certifi==2026.2.25 # via requests cffi==2.0.0 # via cryptography -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -cryptography==46.0.4 +cryptography==46.0.6 # via # joserfc # moto @@ -37,13 +37,13 @@ jmespath==1.1.0 # aws-lambda-powertools # boto3 # botocore -joserfc==1.6.1 +joserfc==1.6.3 # via moto markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[cognitoidp,s3]==5.1.20 +moto[cognitoidp,s3]==5.1.22 # via -r lambdas/python/cognito-backup/requirements-dev.in packaging==26.0 # via pytest @@ -53,7 +53,7 @@ py-partiql-parser==0.6.3 # via moto pycparser==3.0 # via cffi -pygments==2.19.2 +pygments==2.20.0 # via pytest pytest==9.0.2 # via -r lambdas/python/cognito-backup/requirements-dev.in @@ -65,11 +65,11 @@ pyyaml==6.0.3 # via # moto # responses -requests==2.32.5 +requests==2.33.1 # via # moto # responses -responses==0.25.8 +responses==0.26.0 # via moto s3transfer==0.16.0 # via boto3 @@ -82,7 +82,7 @@ urllib3==2.6.3 # botocore # requests # responses -werkzeug==3.1.5 +werkzeug==3.1.8 # via moto -xmltodict==1.0.2 +xmltodict==1.0.4 # via moto diff --git a/backend/compact-connect/lambdas/python/common/requirements-dev.in b/backend/compact-connect/lambdas/python/common/requirements-dev.in index 676fc10bc..6009679f2 100644 --- a/backend/compact-connect/lambdas/python/common/requirements-dev.in +++ b/backend/compact-connect/lambdas/python/common/requirements-dev.in @@ -1,4 +1,6 @@ +# Keep attrs on 25.x to match root requirements.txt (jsii/cattrs); avoids pip-sync conflicts. moto[all]>=5.0.12, <6 boto3-stubs[full] Faker>=37, <38 cryptography>=46, <47 +attrs>=25, <26 diff --git a/backend/compact-connect/lambdas/python/common/requirements-dev.txt b/backend/compact-connect/lambdas/python/common/requirements-dev.txt index e7d219bcf..cf3255bf4 100644 --- a/backend/compact-connect/lambdas/python/common/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/common/requirements-dev.txt @@ -10,6 +10,7 @@ antlr4-python3-runtime==4.13.2 # via moto attrs==25.4.0 # via + # -r lambdas/python/common/requirements-dev.in # jsonschema # referencing aws-sam-translator==1.103.0 @@ -18,15 +19,15 @@ aws-sam-translator==1.103.0 # moto aws-xray-sdk==2.15.0 # via moto -boto3==1.42.42 +boto3==1.42.84 # via # aws-sam-translator # moto -boto3-stubs[full]==1.42.42 +boto3-stubs[full]==1.42.84 # via -r lambdas/python/common/requirements-dev.in -boto3-stubs-full==1.42.41 +boto3-stubs-full==1.42.83 # via boto3-stubs -botocore==1.42.42 +botocore==1.42.84 # via # aws-xray-sdk # boto3 @@ -34,15 +35,15 @@ botocore==1.42.42 # s3transfer botocore-stubs==1.42.41 # via boto3-stubs -certifi==2026.1.4 +certifi==2026.2.25 # via requests cffi==2.0.0 # via cryptography cfn-lint==1.41.0 # via moto -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -cryptography==46.0.4 +cryptography==46.0.6 # via # -r lambdas/python/common/requirements-dev.in # joserfc @@ -51,7 +52,7 @@ docker==7.1.0 # via moto faker==37.12.0 # via -r lambdas/python/common/requirements-dev.in -graphql-core==3.2.7 +graphql-core==3.2.8 # via moto idna==3.11 # via requests @@ -61,21 +62,21 @@ jmespath==1.1.0 # via # boto3 # botocore -joserfc==1.6.1 +joserfc==1.6.3 # via moto jsonpatch==1.33 # via cfn-lint -jsonpath-ng==1.7.0 +jsonpath-ng==1.8.0 # via moto -jsonpointer==3.0.0 +jsonpointer==3.1.1 # via jsonpatch -jsonschema==4.26.0 +jsonschema==4.24.1 # via # aws-sam-translator # moto # openapi-schema-validator # openapi-spec-validator -jsonschema-path==0.3.4 +jsonschema-path==0.4.5 # via openapi-spec-validator jsonschema-specifications==2025.9.1 # via @@ -87,22 +88,20 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[all]==5.1.20 +moto[all]==5.1.22 # via -r lambdas/python/common/requirements-dev.in mpmath==1.3.0 # via sympy -multipart==1.3.0 +multipart==1.3.1 # via moto networkx==3.6.1 # via cfn-lint -openapi-schema-validator==0.6.3 +openapi-schema-validator==0.8.1 # via openapi-spec-validator -openapi-spec-validator==0.7.2 +openapi-spec-validator==0.8.4 # via moto -pathable==0.4.4 +pathable==0.5.0 # via jsonschema-path -ply==3.11 - # via jsonpath-ng py-partiql-parser==0.6.3 # via moto pycparser==3.0 @@ -111,34 +110,43 @@ pydantic==2.12.4 # via # aws-sam-translator # moto + # openapi-schema-validator + # openapi-spec-validator + # pydantic-settings pydantic-core==2.41.5 # via pydantic +pydantic-settings==2.13.1 + # via + # openapi-schema-validator + # openapi-spec-validator pyparsing==3.3.2 # via moto python-dateutil==2.9.0.post0 # via # botocore # moto +python-dotenv==1.2.2 + # via pydantic-settings pyyaml==6.0.3 # via # cfn-lint # jsonschema-path # moto # responses -referencing==0.36.2 +referencing==0.37.0 # via # jsonschema # jsonschema-path # jsonschema-specifications -regex==2026.1.15 + # openapi-schema-validator +regex==2026.4.4 # via cfn-lint -requests==2.32.5 +requests==2.33.1 # via # docker - # jsonschema-path # moto # responses -responses==0.25.8 +responses==0.26.0 # via moto rfc3339-validator==0.1.4 # via openapi-schema-validator @@ -154,7 +162,7 @@ six==1.17.0 # rfc3339-validator sympy==1.14.0 # via cfn-lint -types-awscrt==0.31.1 +types-awscrt==0.31.3 # via botocore-stubs types-s3transfer==0.16.0 # via boto3-stubs @@ -166,8 +174,10 @@ typing-extensions==4.15.0 # pydantic-core # typing-inspection typing-inspection==0.4.2 - # via pydantic -tzdata==2025.3 + # via + # pydantic + # pydantic-settings +tzdata==2026.1 # via faker urllib3==2.6.3 # via @@ -175,11 +185,11 @@ urllib3==2.6.3 # docker # requests # responses -werkzeug==3.1.5 +werkzeug==3.1.8 # via moto -wrapt==2.1.1 +wrapt==2.1.2 # via aws-xray-sdk -xmltodict==1.0.2 +xmltodict==1.0.4 # via moto # The following packages are considered to be unsafe in a requirements file: diff --git a/backend/compact-connect/lambdas/python/common/requirements.txt b/backend/compact-connect/lambdas/python/common/requirements.txt index 6ff410463..328ca596c 100644 --- a/backend/compact-connect/lambdas/python/common/requirements.txt +++ b/backend/compact-connect/lambdas/python/common/requirements.txt @@ -8,23 +8,23 @@ argon2-cffi==25.1.0 # via -r lambdas/python/common/requirements.in argon2-cffi-bindings==25.1.0 # via argon2-cffi -aws-lambda-powertools==3.24.0 +aws-lambda-powertools==3.27.0 # via -r lambdas/python/common/requirements.in -boto3==1.42.42 +boto3==1.42.84 # via -r lambdas/python/common/requirements.in -botocore==1.42.42 +botocore==1.42.84 # via # boto3 # s3transfer -certifi==2026.1.4 +certifi==2026.2.25 # via requests cffi==2.0.0 # via # argon2-cffi-bindings # cryptography -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -cryptography==46.0.4 +cryptography==46.0.6 # via -r lambdas/python/common/requirements.in idna==3.11 # via requests @@ -41,7 +41,7 @@ pycparser==3.0 # via cffi python-dateutil==2.9.0.post0 # via botocore -requests==2.32.5 +requests==2.33.1 # via -r lambdas/python/common/requirements.in s3transfer==0.16.0 # via boto3 diff --git a/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt b/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt index ce862d86b..776e2b17b 100644 --- a/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt @@ -4,20 +4,20 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/compact-configuration/requirements-dev.in # -boto3==1.42.42 +boto3==1.42.84 # via moto -botocore==1.42.42 +botocore==1.42.84 # via # boto3 # moto # s3transfer -certifi==2026.1.4 +certifi==2026.2.25 # via requests cffi==2.0.0 # via cryptography -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -cryptography==46.0.4 +cryptography==46.0.6 # via moto docker==7.1.0 # via moto @@ -33,7 +33,7 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb,s3]==5.1.20 +moto[dynamodb,s3]==5.1.22 # via -r lambdas/python/compact-configuration/requirements-dev.in py-partiql-parser==0.6.3 # via moto @@ -47,12 +47,12 @@ pyyaml==6.0.3 # via # moto # responses -requests==2.32.5 +requests==2.33.1 # via # docker # moto # responses -responses==0.25.8 +responses==0.26.0 # via moto s3transfer==0.16.0 # via boto3 @@ -64,7 +64,7 @@ urllib3==2.6.3 # docker # requests # responses -werkzeug==3.1.5 +werkzeug==3.1.8 # via moto -xmltodict==1.0.2 +xmltodict==1.0.4 # via moto diff --git a/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt b/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt index 52f5198ea..786231f8a 100644 --- a/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt @@ -4,20 +4,20 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/custom-resources/requirements-dev.in # -boto3==1.42.42 +boto3==1.42.84 # via moto -botocore==1.42.42 +botocore==1.42.84 # via # boto3 # moto # s3transfer -certifi==2026.1.4 +certifi==2026.2.25 # via requests cffi==2.0.0 # via cryptography -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -cryptography==46.0.4 +cryptography==46.0.6 # via moto docker==7.1.0 # via moto @@ -33,7 +33,7 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb,s3]==5.1.20 +moto[dynamodb,s3]==5.1.22 # via -r lambdas/python/custom-resources/requirements-dev.in py-partiql-parser==0.6.3 # via moto @@ -47,12 +47,12 @@ pyyaml==6.0.3 # via # moto # responses -requests==2.32.5 +requests==2.33.1 # via # docker # moto # responses -responses==0.25.8 +responses==0.26.0 # via moto s3transfer==0.16.0 # via boto3 @@ -64,7 +64,7 @@ urllib3==2.6.3 # docker # requests # responses -werkzeug==3.1.5 +werkzeug==3.1.8 # via moto -xmltodict==1.0.2 +xmltodict==1.0.4 # via moto diff --git a/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt b/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt index 072d67ec2..1766e2eb8 100644 --- a/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt @@ -4,20 +4,20 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/data-events/requirements-dev.in # -boto3==1.42.42 +boto3==1.42.84 # via moto -botocore==1.42.42 +botocore==1.42.84 # via # boto3 # moto # s3transfer -certifi==2026.1.4 +certifi==2026.2.25 # via requests cffi==2.0.0 # via cryptography -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -cryptography==46.0.4 +cryptography==46.0.6 # via moto docker==7.1.0 # via moto @@ -33,7 +33,7 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb,s3]==5.1.20 +moto[dynamodb,s3]==5.1.22 # via -r lambdas/python/data-events/requirements-dev.in py-partiql-parser==0.6.3 # via moto @@ -47,12 +47,12 @@ pyyaml==6.0.3 # via # moto # responses -requests==2.32.5 +requests==2.33.1 # via # docker # moto # responses -responses==0.25.8 +responses==0.26.0 # via moto s3transfer==0.16.0 # via boto3 @@ -64,7 +64,7 @@ urllib3==2.6.3 # docker # requests # responses -werkzeug==3.1.5 +werkzeug==3.1.8 # via moto -xmltodict==1.0.2 +xmltodict==1.0.4 # via moto diff --git a/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt b/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt index 553a070a6..d9fddb9e3 100644 --- a/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt @@ -4,20 +4,20 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/disaster-recovery/requirements-dev.in # -boto3==1.42.42 +boto3==1.42.84 # via moto -botocore==1.42.42 +botocore==1.42.84 # via # boto3 # moto # s3transfer -certifi==2026.1.4 +certifi==2026.2.25 # via requests cffi==2.0.0 # via cryptography -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -cryptography==46.0.4 +cryptography==46.0.6 # via moto docker==7.1.0 # via moto @@ -33,7 +33,7 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb,s3]==5.1.20 +moto[dynamodb,s3]==5.1.22 # via -r lambdas/python/disaster-recovery/requirements-dev.in py-partiql-parser==0.6.3 # via moto @@ -47,12 +47,12 @@ pyyaml==6.0.3 # via # moto # responses -requests==2.32.5 +requests==2.33.1 # via # docker # moto # responses -responses==0.25.8 +responses==0.26.0 # via moto s3transfer==0.16.0 # via boto3 @@ -64,7 +64,7 @@ urllib3==2.6.3 # docker # requests # responses -werkzeug==3.1.5 +werkzeug==3.1.8 # via moto -xmltodict==1.0.2 +xmltodict==1.0.4 # via moto diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt b/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt index 3c2661b1b..942019f56 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt @@ -4,20 +4,20 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/provider-data-v1/requirements-dev.in # -boto3==1.42.42 +boto3==1.42.84 # via moto -botocore==1.42.42 +botocore==1.42.84 # via # boto3 # moto # s3transfer -certifi==2026.1.4 +certifi==2026.2.25 # via requests cffi==2.0.0 # via cryptography -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -cryptography==46.0.4 +cryptography==46.0.6 # via moto docker==7.1.0 # via moto @@ -35,7 +35,7 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb,s3]==5.1.20 +moto[dynamodb,s3]==5.1.22 # via -r lambdas/python/provider-data-v1/requirements-dev.in py-partiql-parser==0.6.3 # via moto @@ -49,18 +49,18 @@ pyyaml==6.0.3 # via # moto # responses -requests==2.32.5 +requests==2.33.1 # via # docker # moto # responses -responses==0.25.8 +responses==0.26.0 # via moto s3transfer==0.16.0 # via boto3 six==1.17.0 # via python-dateutil -tzdata==2025.3 +tzdata==2026.1 # via faker urllib3==2.6.3 # via @@ -68,7 +68,7 @@ urllib3==2.6.3 # docker # requests # responses -werkzeug==3.1.5 +werkzeug==3.1.8 # via moto -xmltodict==1.0.2 +xmltodict==1.0.4 # via moto diff --git a/backend/compact-connect/lambdas/python/search/requirements-dev.txt b/backend/compact-connect/lambdas/python/search/requirements-dev.txt index a8ef8fb85..34cf70918 100644 --- a/backend/compact-connect/lambdas/python/search/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/search/requirements-dev.txt @@ -4,20 +4,20 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/search/requirements-dev.in # -boto3==1.42.42 +boto3==1.42.84 # via moto -botocore==1.42.42 +botocore==1.42.84 # via # boto3 # moto # s3transfer -certifi==2026.1.4 +certifi==2026.2.25 # via requests cffi==2.0.0 # via cryptography -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -cryptography==46.0.4 +cryptography==46.0.6 # via moto docker==7.1.0 # via moto @@ -33,7 +33,7 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb]==5.1.20 +moto[dynamodb]==5.1.22 # via -r lambdas/python/search/requirements-dev.in py-partiql-parser==0.6.3 # via moto @@ -45,12 +45,12 @@ python-dateutil==2.9.0.post0 # moto pyyaml==6.0.3 # via responses -requests==2.32.5 +requests==2.33.1 # via # docker # moto # responses -responses==0.25.8 +responses==0.26.0 # via moto s3transfer==0.16.0 # via boto3 @@ -62,7 +62,7 @@ urllib3==2.6.3 # docker # requests # responses -werkzeug==3.1.5 +werkzeug==3.1.8 # via moto -xmltodict==1.0.2 +xmltodict==1.0.4 # via moto diff --git a/backend/compact-connect/lambdas/python/search/requirements.txt b/backend/compact-connect/lambdas/python/search/requirements.txt index c16489c7c..1df821788 100644 --- a/backend/compact-connect/lambdas/python/search/requirements.txt +++ b/backend/compact-connect/lambdas/python/search/requirements.txt @@ -4,15 +4,15 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/search/requirements.in # -certifi==2026.1.4 +certifi==2026.2.25 # via # opensearch-py # requests -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests events==0.5 # via opensearch-py -grpcio==1.76.0 +grpcio==1.80.0 # via opensearch-protobufs idna==3.11 # via requests @@ -20,11 +20,11 @@ opensearch-protobufs==0.19.0 # via opensearch-py opensearch-py==3.1.0 # via -r lambdas/python/search/requirements.in -protobuf==6.33.5 +protobuf==7.34.1 # via opensearch-protobufs python-dateutil==2.9.0.post0 # via opensearch-py -requests==2.32.5 +requests==2.33.1 # via opensearch-py six==1.17.0 # via python-dateutil diff --git a/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt b/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt index 65523ffcd..f3a4d6b83 100644 --- a/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt @@ -4,20 +4,20 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/staff-user-pre-token/requirements-dev.in # -boto3==1.42.42 +boto3==1.42.84 # via moto -botocore==1.42.42 +botocore==1.42.84 # via # boto3 # moto # s3transfer -certifi==2026.1.4 +certifi==2026.2.25 # via requests cffi==2.0.0 # via cryptography -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -cryptography==46.0.4 +cryptography==46.0.6 # via moto docker==7.1.0 # via moto @@ -33,7 +33,7 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb,s3]==5.1.20 +moto[dynamodb,s3]==5.1.22 # via -r lambdas/python/staff-user-pre-token/requirements-dev.in py-partiql-parser==0.6.3 # via moto @@ -47,12 +47,12 @@ pyyaml==6.0.3 # via # moto # responses -requests==2.32.5 +requests==2.33.1 # via # docker # moto # responses -responses==0.25.8 +responses==0.26.0 # via moto s3transfer==0.16.0 # via boto3 @@ -64,7 +64,7 @@ urllib3==2.6.3 # docker # requests # responses -werkzeug==3.1.5 +werkzeug==3.1.8 # via moto -xmltodict==1.0.2 +xmltodict==1.0.4 # via moto diff --git a/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt b/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt index 0c39fa9b6..05d852394 100644 --- a/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt @@ -4,20 +4,20 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/staff-users/requirements-dev.in # -boto3==1.42.42 +boto3==1.42.84 # via moto -botocore==1.42.42 +botocore==1.42.84 # via # boto3 # moto # s3transfer -certifi==2026.1.4 +certifi==2026.2.25 # via requests cffi==2.0.0 # via cryptography -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -cryptography==46.0.4 +cryptography==46.0.6 # via # joserfc # moto @@ -33,13 +33,13 @@ jmespath==1.1.0 # via # boto3 # botocore -joserfc==1.6.1 +joserfc==1.6.3 # via moto markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[cognitoidp,dynamodb,s3]==5.1.20 +moto[cognitoidp,dynamodb,s3]==5.1.22 # via -r lambdas/python/staff-users/requirements-dev.in py-partiql-parser==0.6.3 # via moto @@ -53,18 +53,18 @@ pyyaml==6.0.3 # via # moto # responses -requests==2.32.5 +requests==2.33.1 # via # docker # moto # responses -responses==0.25.8 +responses==0.26.0 # via moto s3transfer==0.16.0 # via boto3 six==1.17.0 # via python-dateutil -tzdata==2025.3 +tzdata==2026.1 # via faker urllib3==2.6.3 # via @@ -72,7 +72,7 @@ urllib3==2.6.3 # docker # requests # responses -werkzeug==3.1.5 +werkzeug==3.1.8 # via moto -xmltodict==1.0.2 +xmltodict==1.0.4 # via moto diff --git a/backend/compact-connect/requirements-dev.txt b/backend/compact-connect/requirements-dev.txt index 0cd970211..cc6277dad 100644 --- a/backend/compact-connect/requirements-dev.txt +++ b/backend/compact-connect/requirements-dev.txt @@ -6,29 +6,29 @@ # boolean-py==5.0 # via license-expression -build==1.4.0 +build==1.4.2 # via pip-tools cachecontrol[filecache]==0.14.4 # via # cachecontrol # pip-audit -certifi==2026.1.4 +certifi==2026.2.25 # via requests -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -click==8.3.1 +click==8.3.2 # via pip-tools -coverage[toml]==7.13.3 +coverage[toml]==7.13.5 # via # -r requirements-dev.in # pytest-cov -cyclonedx-python-lib==11.6.0 +cyclonedx-python-lib==11.7.0 # via pip-audit defusedxml==0.7.1 # via py-serializable faker==37.12.0 # via -r requirements-dev.in -filelock==3.20.3 +filelock==3.25.2 # via cachecontrol idna==3.11 # via requests @@ -57,9 +57,9 @@ pip-audit==2.10.0 # via -r requirements-dev.in pip-requirements-parser==32.0.1 # via pip-audit -pip-tools==7.5.2 +pip-tools==7.5.3 # via -r requirements-dev.in -platformdirs==4.5.1 +platformdirs==4.9.4 # via pip-audit pluggy==1.6.0 # via @@ -67,7 +67,7 @@ pluggy==1.6.0 # pytest-cov py-serializable==2.1.0 # via cyclonedx-python-lib -pygments==2.19.2 +pygments==2.20.0 # via # pytest # rich @@ -81,23 +81,23 @@ pytest==9.0.2 # via # -r requirements-dev.in # pytest-cov -pytest-cov==7.0.0 +pytest-cov==7.1.0 # via -r requirements-dev.in -requests==2.32.5 +requests==2.33.1 # via # cachecontrol # pip-audit -rich==14.3.2 +rich==14.3.3 # via pip-audit -ruff==0.15.0 +ruff==0.15.9 # via -r requirements-dev.in sortedcontainers==2.4.0 # via cyclonedx-python-lib -tomli==2.4.0 +tomli==2.4.1 # via pip-audit tomli-w==1.2.0 # via pip-audit -tzdata==2025.3 +tzdata==2026.1 # via faker urllib3==2.6.3 # via requests diff --git a/backend/compact-connect/requirements.txt b/backend/compact-connect/requirements.txt index c73efbc3c..c3630a863 100644 --- a/backend/compact-connect/requirements.txt +++ b/backend/compact-connect/requirements.txt @@ -8,15 +8,15 @@ attrs==25.4.0 # via # cattrs # jsii -aws-cdk-asset-awscli-v1==2.2.263 +aws-cdk-asset-awscli-v1==2.2.273 # via aws-cdk-lib -aws-cdk-asset-node-proxy-agent-v6==2.1.0 +aws-cdk-asset-node-proxy-agent-v6==2.1.1 # via aws-cdk-lib -aws-cdk-aws-lambda-python-alpha==2.237.1a0 +aws-cdk-aws-lambda-python-alpha==2.248.0a0 # via -r requirements.in -aws-cdk-cloud-assembly-schema==48.20.0 +aws-cdk-cloud-assembly-schema==53.13.0 # via aws-cdk-lib -aws-cdk-lib==2.237.1 +aws-cdk-lib==2.248.0 # via # -r requirements.in # aws-cdk-aws-lambda-python-alpha @@ -25,7 +25,7 @@ cattrs==25.3.0 # via jsii cdk-nag==2.37.55 # via -r requirements.in -constructs==10.4.5 +constructs==10.6.0 # via # -r requirements.in # aws-cdk-aws-lambda-python-alpha @@ -33,7 +33,7 @@ constructs==10.4.5 # cdk-nag importlib-resources==6.5.2 # via jsii -jsii==1.126.0 +jsii==1.127.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-node-proxy-agent-v6 From eff3ed949b52ad3ada66179980a7c69bd1b2c914 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Apr 2026 16:29:26 -0500 Subject: [PATCH 03/15] Update purchase directory python requirements --- .../python/purchases/requirements-dev.txt | 38 +++++++++---------- .../lambdas/python/purchases/requirements.txt | 6 +-- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt b/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt index 44b3730ec..235685b09 100644 --- a/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt @@ -8,20 +8,20 @@ argon2-cffi==25.1.0 # via -r requirements-dev.in argon2-cffi-bindings==25.1.0 # via argon2-cffi -aws-lambda-powertools==3.24.0 +aws-lambda-powertools==3.27.0 # via -r requirements-dev.in boolean-py==5.0 # via license-expression -boto3==1.42.58 +boto3==1.42.84 # via # -r requirements-dev.in # moto -botocore==1.42.58 +botocore==1.42.84 # via # boto3 # moto # s3transfer -build==1.4.0 +build==1.4.2 # via pip-tools cachecontrol[filecache]==0.14.4 # via @@ -33,19 +33,19 @@ cffi==2.0.0 # via # argon2-cffi-bindings # cryptography -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests -click==8.3.1 +click==8.3.2 # via pip-tools -coverage[toml]==7.13.4 +coverage[toml]==7.13.5 # via # -r requirements-dev.in # pytest-cov -cryptography==46.0.5 +cryptography==46.0.6 # via # -r requirements-dev.in # moto -cyclonedx-python-lib==11.6.0 +cyclonedx-python-lib==11.7.0 # via pip-audit defusedxml==0.7.1 # via py-serializable @@ -53,7 +53,7 @@ docker==7.1.0 # via moto faker==37.12.0 # via -r requirements-dev.in -filelock==3.24.3 +filelock==3.25.2 # via cachecontrol idna==3.11 # via requests @@ -78,7 +78,7 @@ marshmallow==3.26.2 # via -r requirements-dev.in mdurl==0.1.2 # via markdown-it-py -moto[dynamodb,s3]==5.1.21 +moto[dynamodb,s3]==5.1.22 # via -r requirements-dev.in msgpack==1.1.2 # via cachecontrol @@ -100,7 +100,7 @@ pip-requirements-parser==32.0.1 # via pip-audit pip-tools==7.5.3 # via -r requirements-dev.in -platformdirs==4.9.2 +platformdirs==4.9.4 # via pip-audit pluggy==1.6.0 # via @@ -112,7 +112,7 @@ py-serializable==2.1.0 # via cyclonedx-python-lib pycparser==3.0 # via cffi -pygments==2.19.2 +pygments==2.20.0 # via # pytest # rich @@ -126,7 +126,7 @@ pytest==9.0.2 # via # -r requirements-dev.in # pytest-cov -pytest-cov==7.0.0 +pytest-cov==7.1.0 # via -r requirements-dev.in python-dateutil==2.9.0.post0 # via @@ -136,7 +136,7 @@ pyyaml==6.0.3 # via # moto # responses -requests==2.32.5 +requests==2.33.1 # via # -r requirements-dev.in # cachecontrol @@ -148,7 +148,7 @@ responses==0.26.0 # via moto rich==14.3.3 # via pip-audit -ruff==0.15.4 +ruff==0.15.9 # via -r requirements-dev.in s3transfer==0.16.0 # via boto3 @@ -156,7 +156,7 @@ six==1.17.0 # via python-dateutil sortedcontainers==2.4.0 # via cyclonedx-python-lib -tomli==2.4.0 +tomli==2.4.1 # via pip-audit tomli-w==1.2.0 # via pip-audit @@ -164,7 +164,7 @@ typing-extensions==4.15.0 # via # aws-lambda-powertools # cyclonedx-python-lib -tzdata==2025.3 +tzdata==2026.1 # via faker urllib3==2.6.3 # via @@ -173,7 +173,7 @@ urllib3==2.6.3 # docker # requests # responses -werkzeug==3.1.6 +werkzeug==3.1.8 # via moto wheel==0.46.3 # via pip-tools diff --git a/backend/compact-connect/lambdas/python/purchases/requirements.txt b/backend/compact-connect/lambdas/python/purchases/requirements.txt index 0428304da..e921e9e66 100644 --- a/backend/compact-connect/lambdas/python/purchases/requirements.txt +++ b/backend/compact-connect/lambdas/python/purchases/requirements.txt @@ -6,9 +6,9 @@ # authorizenet==1.1.6 # via -r requirements.in -certifi==2026.1.4 +certifi==2026.2.25 # via requests -charset-normalizer==3.4.4 +charset-normalizer==3.4.7 # via requests idna==3.11 # via requests @@ -16,7 +16,7 @@ lxml==4.9.4 # via authorizenet pyxb-x==1.2.6.3 # via authorizenet -requests==2.32.5 +requests==2.33.1 # via authorizenet urllib3==2.6.3 # via From 8208df878e1500650c0f733ee4a07483c98d2aa9 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Apr 2026 16:51:52 -0500 Subject: [PATCH 04/15] update timestamps for other military flows --- .../common/cc_common/data_model/data_client.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py index c0092d299..781708cb5 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py @@ -732,12 +732,14 @@ def complete_military_affiliation_initialization(self, compact: str, provider_id 'UpdateExpression': ( 'SET militaryStatus = :militaryStatus, ' 'militaryStatusNote = :militaryStatusNote, ' - 'dateOfUpdate = :dateOfUpdate' + 'dateOfUpdate = :dateOfUpdate, ' + 'providerDateOfUpdate = :providerDateOfUpdate' ), 'ExpressionAttributeValues': { ':militaryStatus': {'S': MilitaryStatus.TENTATIVE}, ':militaryStatusNote': {'S': ''}, - ':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + ':dateOfUpdate': {'S': now.isoformat()}, + ':providerDateOfUpdate': {'S': now.isoformat()}, }, 'ConditionExpression': 'attribute_exists(pk)', } @@ -770,7 +772,7 @@ def complete_military_affiliation_initialization(self, compact: str, provider_id 'ExpressionAttributeNames': {'#status': 'status'}, 'ExpressionAttributeValues': { ':status': {'S': status_value}, - ':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + ':dateOfUpdate': {'S': now.isoformat()}, }, } } @@ -912,8 +914,8 @@ def end_military_affiliation(self, compact: str, provider_id: str) -> None: 'REMOVE militaryStatus, militaryStatusNote' ), 'ExpressionAttributeValues': { - ':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, - ':providerDateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + ':dateOfUpdate': {'S': now.isoformat()}, + ':providerDateOfUpdate': {'S': now.isoformat()}, }, 'ConditionExpression': 'attribute_exists(pk)', } @@ -1049,12 +1051,14 @@ def process_military_audit( 'UpdateExpression': ( 'SET militaryStatus = :militaryStatus, ' 'militaryStatusNote = :militaryStatusNote, ' - 'dateOfUpdate = :dateOfUpdate' + 'dateOfUpdate = :dateOfUpdate, ' + 'providerDateOfUpdate = :providerDateOfUpdate' ), 'ExpressionAttributeValues': { ':militaryStatus': {'S': military_status}, ':militaryStatusNote': {'S': note_value}, ':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + ':providerDateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, }, 'ConditionExpression': 'attribute_exists(pk)', } From aded5f58f1b78c2551a908426dcbfeead358818c Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Apr 2026 16:58:42 -0500 Subject: [PATCH 05/15] PR feedback - set explicit mock time in test --- .../test_handlers/test_purchase_privileges.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py b/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py index 28cc8a9de..48207cf38 100644 --- a/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py +++ b/backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py @@ -1256,6 +1256,20 @@ def test_post_purchase_privileges_updates_provider_date_of_update(self, mock_pur event = self._when_testing_provider_user_event_with_custom_claims(license_expiration_date='2050-01-01') event['body'] = _generate_test_request_body() + # provider with stale datetime that should be updated to the mock current time + stale_datetime = '2000-01-01T00:00:00+00:00' + self._provider_table.update_item( + Key={ + 'pk': f'{TEST_COMPACT}#PROVIDER#{TEST_PROVIDER_ID}', + 'sk': f'{TEST_COMPACT}#PROVIDER', + }, + UpdateExpression='SET dateOfUpdate = :d, providerDateOfUpdate = :p', + ExpressionAttributeValues={ + ':d': stale_datetime, + ':p': stale_datetime, + }, + ) + resp = post_purchase_privileges(event, self.mock_context) self.assertEqual(200, resp['statusCode'], resp['body']) From a61da0fa7601848169b296941bcac2ab5296e32d Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Apr 2026 17:07:14 -0500 Subject: [PATCH 06/15] PR feedback - using same timestamp reference --- .../cc_common/data_model/data_client.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py index 781708cb5..db1078b28 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py @@ -1057,8 +1057,8 @@ def process_military_audit( 'ExpressionAttributeValues': { ':militaryStatus': {'S': military_status}, ':militaryStatusNote': {'S': note_value}, - ':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, - ':providerDateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + ':dateOfUpdate': {'S': now.isoformat()}, + ':providerDateOfUpdate': {'S': now.isoformat()}, }, 'ConditionExpression': 'attribute_exists(pk)', } @@ -1486,7 +1486,7 @@ def deactivate_privilege( 'UpdateExpression': 'SET administratorSetStatus = :status, dateOfUpdate = :dateOfUpdate', 'ExpressionAttributeValues': { ':status': {'S': ActiveInactiveStatus.INACTIVE}, - ':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + ':dateOfUpdate': {'S': now.isoformat()}, }, }, }, @@ -1501,8 +1501,8 @@ def deactivate_privilege( 'UpdateExpression': 'SET dateOfUpdate = :dateOfUpdate, ' 'providerDateOfUpdate = :providerDateOfUpdate', 'ExpressionAttributeValues': { - ':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, - ':providerDateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + ':dateOfUpdate': {'S': now.isoformat()}, + ':providerDateOfUpdate': {'S': now.isoformat()}, }, }, }, @@ -1590,6 +1590,7 @@ def _generate_set_provider_encumbered_status_item( provider_encumbered_status: LicenseEncumberedStatusEnum, ): data_record = provider_data.serialize_to_database_record() + now = self.config.current_standard_datetime return { 'Update': { 'TableName': self.config.provider_table.name, @@ -1598,8 +1599,8 @@ def _generate_set_provider_encumbered_status_item( 'providerDateOfUpdate = :providerDateOfUpdate', 'ExpressionAttributeValues': { ':status': {'S': provider_encumbered_status}, - ':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, - ':providerDateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + ':dateOfUpdate': {'S': now.isoformat()}, + ':providerDateOfUpdate': {'S': now.isoformat()}, }, }, } @@ -3008,8 +3009,8 @@ def _get_provider_record_transaction_items_for_jurisdiction_with_no_known_licens 'providerDateOfUpdate = :providerDateOfUpdate', 'ExpressionAttributeValues': { ':currentHomeJurisdiction': {'S': selected_jurisdiction}, - ':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, - ':providerDateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()}, + ':dateOfUpdate': {'S': now.isoformat()}, + ':providerDateOfUpdate': {'S': now.isoformat()}, }, } }, From 296191072dd54540e475a2c9c5af3a5fe7635ef6 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Apr 2026 17:22:12 -0500 Subject: [PATCH 07/15] update timestamp when provider changes email --- .../cc_common/data_model/data_client.py | 6 ++- .../test_provider_users_email.py | 45 ++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py index db1078b28..cb6fa6fbb 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py @@ -3892,12 +3892,14 @@ def complete_provider_email_update( }, 'UpdateExpression': ( 'SET compactConnectRegisteredEmailAddress = :new_email, ' - 'dateOfUpdate = :date_of_update ' + 'dateOfUpdate = :date_of_update, ' + 'providerDateOfUpdate = :provider_date_of_update ' 'REMOVE pendingEmailAddress, emailVerificationCode, emailVerificationExpiry' ), 'ExpressionAttributeValues': { ':new_email': {'S': new_email_address}, - ':date_of_update': {'S': self.config.current_standard_datetime.isoformat()}, + ':date_of_update': {'S': now.isoformat()}, + ':provider_date_of_update': {'S': now.isoformat()}, }, # Ensure the provider record exists before updating 'ConditionExpression': 'attribute_exists(pk)', diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_email.py b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_email.py index cfddc3450..d37f368bc 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_email.py +++ b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_email.py @@ -5,7 +5,7 @@ from unittest.mock import patch from cc_common.exceptions import CCInternalException -from common_test.test_constants import DEFAULT_COMPACT, DEFAULT_PROVIDER_ID +from common_test.test_constants import DEFAULT_COMPACT, DEFAULT_DATE_OF_UPDATE_TIMESTAMP, DEFAULT_PROVIDER_ID from moto import mock_aws from .. import TstFunction @@ -443,6 +443,49 @@ def test_endpoint_updates_dynamo_provider_record_with_expected_values(self, mock self.assertIsNone(stored_provider_data.emailVerificationCode) self.assertIsNone(stored_provider_data.emailVerificationExpiry) + @patch('cc_common.config._Config.email_service_client') + def test_complete_provider_email_update_sets_provider_date_of_update_on_provider_record(self, mock_email_service_client): + from handlers.provider_users import provider_users_api_handler + + self.config.cognito_client.admin_create_user( + UserPoolId=self.config.provider_user_pool_id, + Username=TEST_OLD_EMAIL, + UserAttributes=[ + {'Name': 'email', 'Value': TEST_OLD_EMAIL}, + {'Name': 'email_verified', 'Value': 'true'}, + {'Name': 'custom:compact', 'Value': DEFAULT_COMPACT}, + {'Name': 'custom:providerId', 'Value': DEFAULT_PROVIDER_ID}, + ], + MessageAction='SUPPRESS', + ) + + event = self._when_testing_provider_user_event_with_custom_claims() + stale_datetime = '2000-01-01T00:00:00+00:00' + self._provider_table.update_item( + Key={ + 'pk': f'{DEFAULT_COMPACT}#PROVIDER#{DEFAULT_PROVIDER_ID}', + 'sk': f'{DEFAULT_COMPACT}#PROVIDER', + }, + UpdateExpression='SET dateOfUpdate = :d, providerDateOfUpdate = :p', + ExpressionAttributeValues={ + ':d': stale_datetime, + ':p': stale_datetime, + }, + ) + + resp = provider_users_api_handler(event, self.mock_context) + self.assertEqual(200, resp['statusCode']) + + provider_item = self._provider_table.get_item( + Key={ + 'pk': f'{DEFAULT_COMPACT}#PROVIDER#{DEFAULT_PROVIDER_ID}', + 'sk': f'{DEFAULT_COMPACT}#PROVIDER', + } + )['Item'] + + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_item['dateOfUpdate']) + self.assertEqual(DEFAULT_DATE_OF_UPDATE_TIMESTAMP, provider_item['providerDateOfUpdate']) + @patch('cc_common.config._Config.email_service_client') def test_endpoint_creates_dynamo_provider_update_record_with_expected_values(self, mock_email_service_client): from cc_common.data_model.schema.provider import ProviderUpdateData From d7c9c76319bd8caa323edde24eeae0d8a5c98bc6 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Apr 2026 17:24:18 -0500 Subject: [PATCH 08/15] formatting --- .../tests/function/test_handlers/test_provider_users_email.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_email.py b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_email.py index d37f368bc..03d2df27d 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_email.py +++ b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_email.py @@ -444,7 +444,9 @@ def test_endpoint_updates_dynamo_provider_record_with_expected_values(self, mock self.assertIsNone(stored_provider_data.emailVerificationExpiry) @patch('cc_common.config._Config.email_service_client') - def test_complete_provider_email_update_sets_provider_date_of_update_on_provider_record(self, mock_email_service_client): + def test_complete_provider_email_update_sets_provider_date_of_update_on_provider_record( + self, mock_email_service_client + ): from handlers.provider_users import provider_users_api_handler self.config.cognito_client.admin_create_user( From c4a461cee71896e3205505b2ae1787194ce6551c Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Apr 2026 22:15:02 -0500 Subject: [PATCH 09/15] PR feedback / update smoke test --- .../common/cc_common/data_model/data_client.py | 12 ++++++++++-- .../home_jurisdiction_change_smoke_tests.py | 16 ++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py index cb6fa6fbb..05b9b98cf 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py @@ -421,6 +421,14 @@ def create_provider_privileges( privilege_jurisdictions=jurisdiction_postal_abbreviations, ) + if not jurisdiction_postal_abbreviations: + logger.error('No list of jurisdictions provided. Cannot generate privileges for empty list.', + compact=compact, + provider_id=provider_id, + compact_transaction_id=compact_transaction_id, + ) + raise CCInternalException('No list of jurisdictions provided. Cannot generate privileges for empty list.') + license_jurisdiction = provider_record.licenseJurisdiction privileges = [] @@ -431,6 +439,8 @@ def create_provider_privileges( processed_transactions = [] privilege_update_records = [] + now = config.current_standard_datetime + for postal_abbreviation in jurisdiction_postal_abbreviations: # get the original privilege issuance date from an existing privilege record if it exists original_privilege = next( @@ -457,8 +467,6 @@ def create_provider_privileges( privileges.append(privilege_record) - now = config.current_standard_datetime - # Create privilege update record if this is updating an existing privilege if original_privilege: update_record = PrivilegeUpdateData.create_new( diff --git a/backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py b/backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py index d5fa30f16..2b07afc54 100644 --- a/backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py +++ b/backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py @@ -11,7 +11,7 @@ call_provider_users_me_endpoint, get_provider_user_auth_headers_cached, get_provider_user_records, - load_smoke_test_env, + load_smoke_test_env, get_license_type_abbreviation, ) # This script can be run locally to test the home jurisdiction change flow against a sandbox environment @@ -205,7 +205,7 @@ def add_license_for_provider(provider_record: dict, jurisdiction: str): compact = provider_record['compact'] license_record = { 'pk': f'{compact}#PROVIDER#{provider_id}', - 'sk': f'{compact}#PROVIDER#license/{jurisdiction}/{license_type}#', + 'sk': f'{compact}#PROVIDER#license/{jurisdiction}/{get_license_type_abbreviation(license_type)}#', 'type': 'license', 'providerId': provider_id, 'compact': compact, @@ -213,7 +213,7 @@ def add_license_for_provider(provider_record: dict, jurisdiction: str): 'ssnLastFour': '1234', 'npi': '0608337260', 'licenseNumber': 'A0608337260', - 'licenseType': 'speech-language pathologist', + 'licenseType': license_type, 'givenName': 'Björk', 'middleName': 'Gunnar', 'familyName': 'Guðmundsdóttir', @@ -225,14 +225,14 @@ def add_license_for_provider(provider_record: dict, jurisdiction: str): 'homeAddressStreet1': '123 A St.', 'homeAddressStreet2': 'Apt 321', 'homeAddressCity': 'Columbus', - 'homeAddressState': 'oh', + 'homeAddressState': jurisdiction, 'homeAddressPostalCode': '43004', 'emailAddress': 'björk@example.com', 'phoneNumber': '+13213214321', 'jurisdictionUploadedLicenseStatus': 'active', 'licenseStatusName': 'DEFINITELY_A_HUMAN', 'jurisdictionUploadedCompactEligibility': 'eligible', - 'licenseGSIPK': 'C#aslp#J#oh', + 'licenseGSIPK': f'C#{compact}#J#{jurisdiction}', 'licenseGSISK': 'FN#gu%C3%B0mundsd%C3%B3ttir#GN#bj%C3%B6rk', } # put the license in for the new jurisdiction @@ -249,6 +249,10 @@ def test_home_jurisdiction_change_moves_privileges_when_valid_license_in_new_jur Test that when a provider changes their home jurisdiction to a jurisdiction where they have a valid license: 1. All their privileges are set to active 2. Their compactEligibility on the provider record is set to eligible + + NOTE: this test assumes that the compact your test provider is registered in has Alabama as a live state in + its configuration, if that state is not live in your environment for that compact, you will need to add it or + change the jurisdiction """ logger.info('Running home jurisdiction change test - changing to jurisdiction with valid license') @@ -257,7 +261,7 @@ def test_home_jurisdiction_change_moves_privileges_when_valid_license_in_new_jur original_jurisdiction = provider_info_before.get('currentHomeJurisdiction') original_expiration_date = provider_info_before['licenses'][0]['dateOfExpiration'] - new_jurisdiction = 'al' # Alabama - assuming the provider doesn't have a license here + new_jurisdiction = 'ar' # Alabama - assuming the provider doesn't have a license here logger.info(f'Original home jurisdiction: {original_jurisdiction}') # create jurisdiction config for AL From 52f9b80f057813c27ba7ddd855f6d8293824826f Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Apr 2026 22:16:53 -0500 Subject: [PATCH 10/15] formatting --- .../python/common/cc_common/data_model/data_client.py | 9 +++++---- .../tests/smoke/home_jurisdiction_change_smoke_tests.py | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py index 05b9b98cf..8180e2175 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py @@ -422,10 +422,11 @@ def create_provider_privileges( ) if not jurisdiction_postal_abbreviations: - logger.error('No list of jurisdictions provided. Cannot generate privileges for empty list.', - compact=compact, - provider_id=provider_id, - compact_transaction_id=compact_transaction_id, + logger.error( + 'No list of jurisdictions provided. Cannot generate privileges for empty list.', + compact=compact, + provider_id=provider_id, + compact_transaction_id=compact_transaction_id, ) raise CCInternalException('No list of jurisdictions provided. Cannot generate privileges for empty list.') diff --git a/backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py b/backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py index 2b07afc54..5a79a5111 100644 --- a/backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py +++ b/backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py @@ -9,9 +9,10 @@ from smoke_common import ( SmokeTestFailureException, call_provider_users_me_endpoint, + get_license_type_abbreviation, get_provider_user_auth_headers_cached, get_provider_user_records, - load_smoke_test_env, get_license_type_abbreviation, + load_smoke_test_env, ) # This script can be run locally to test the home jurisdiction change flow against a sandbox environment From 2adea90a5c12a4e550515122eb6c4b150e474b95 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 7 Apr 2026 09:29:10 -0500 Subject: [PATCH 11/15] PR feedback --- .../tests/function/test_handlers/test_encumbrance.py | 5 ++++- .../tests/smoke/home_jurisdiction_change_smoke_tests.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py index 72bbd97a5..49f0b85c4 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py +++ b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py @@ -812,7 +812,9 @@ def test_should_not_update_provider_record_when_other_privilege_encumbrances_exi from handlers.encumbrance import encumbrance_handler # Set up first privilege with adverse action - privilege_record, adverse_action = self._setup_privilege_with_adverse_action() + privilege_record, adverse_action = self._setup_privilege_with_adverse_action( + date_of_update_override='2020-01-01T00:00:00+00:00' + ) # Set up second privilege with encumbered status (different jurisdiction) self.test_data_generator.put_default_privilege_record_in_provider_table( @@ -836,6 +838,7 @@ def test_should_not_update_provider_record_when_other_privilege_encumbrances_exi loaded_provider_data = provider_records.get_provider_record() self.assertEqual(LicenseEncumberedStatusEnum.ENCUMBERED, loaded_provider_data.encumberedStatus) + self.assertEqual('2020-01-01T00:00:00+00:00', loaded_provider_data.dateOfUpdate.isoformat()) def test_should_not_update_provider_record_when_encumbered_license_exists(self): from cc_common.data_model.provider_record_util import ProviderUserRecords diff --git a/backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py b/backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py index 5a79a5111..cf0f60e5d 100644 --- a/backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py +++ b/backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py @@ -262,7 +262,7 @@ def test_home_jurisdiction_change_moves_privileges_when_valid_license_in_new_jur original_jurisdiction = provider_info_before.get('currentHomeJurisdiction') original_expiration_date = provider_info_before['licenses'][0]['dateOfExpiration'] - new_jurisdiction = 'ar' # Alabama - assuming the provider doesn't have a license here + new_jurisdiction = 'al' # Alabama - assuming the provider doesn't have a license here logger.info(f'Original home jurisdiction: {original_jurisdiction}') # create jurisdiction config for AL From 87cfd06cce8a94192a952ca8ea63ee25a5d28f21 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 7 Apr 2026 09:37:15 -0500 Subject: [PATCH 12/15] PR feedback - add condition expression to enforce record exists --- .../lambdas/python/common/cc_common/data_model/data_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py index 8180e2175..367707b9b 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py @@ -544,6 +544,7 @@ def create_provider_privileges( ':dateOfUpdate': {'S': now.isoformat()}, ':providerDateOfUpdate': {'S': now.isoformat()}, }, + 'ConditionExpression': 'attribute_exists(pk)', } } ) @@ -1513,6 +1514,7 @@ def deactivate_privilege( ':dateOfUpdate': {'S': now.isoformat()}, ':providerDateOfUpdate': {'S': now.isoformat()}, }, + 'ConditionExpression': 'attribute_exists(pk)', }, }, # Create a history record, reflecting this change @@ -1611,6 +1613,7 @@ def _generate_set_provider_encumbered_status_item( ':dateOfUpdate': {'S': now.isoformat()}, ':providerDateOfUpdate': {'S': now.isoformat()}, }, + 'ConditionExpression': 'attribute_exists(pk)', }, } @@ -3021,6 +3024,7 @@ def _get_provider_record_transaction_items_for_jurisdiction_with_no_known_licens ':dateOfUpdate': {'S': now.isoformat()}, ':providerDateOfUpdate': {'S': now.isoformat()}, }, + 'ConditionExpression': 'attribute_exists(pk)', } }, ] From fe44a9b1992b2f9ccc44caa8a6d50c73d346a861 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 7 Apr 2026 10:37:16 -0500 Subject: [PATCH 13/15] Add check for 401 status code to refresh token --- backend/compact-connect/tests/smoke/smoke_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/compact-connect/tests/smoke/smoke_common.py b/backend/compact-connect/tests/smoke/smoke_common.py index 7ee25bb60..6743b459d 100644 --- a/backend/compact-connect/tests/smoke/smoke_common.py +++ b/backend/compact-connect/tests/smoke/smoke_common.py @@ -243,7 +243,7 @@ def call_provider_users_me_endpoint(): ) # If we get a 403, the token may have expired - refresh it and retry once - if get_provider_data_response.status_code == 403: + if get_provider_data_response.status_code == 403 or get_provider_data_response.status_code == 401: logger.info('Received 403 response, refreshing provider user token and retrying...') # Clear the cached token to force a refresh if 'TEST_PROVIDER_USER_ID_TOKEN' in os.environ: From 6e895b24d7fd68536e0c20552aa143eafb9817fc Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 7 Apr 2026 11:43:54 -0500 Subject: [PATCH 14/15] setup test provider records to pass condition checks --- .../common/tests/function/test_data_client.py | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py b/backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py index b17bd1aa1..69fd65375 100644 --- a/backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py +++ b/backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py @@ -6,7 +6,7 @@ from boto3.dynamodb.conditions import Key from cc_common.data_model.update_tier_enum import UpdateTierEnum from cc_common.exceptions import CCAwsServiceException, CCInvalidRequestException -from common_test.test_constants import DEFAULT_PROVIDER_ID +from common_test.test_constants import DEFAULT_PROVIDER_ID, DEFAULT_LICENSE_JURISDICTION from moto import mock_aws from tests.function import TstFunction @@ -152,14 +152,18 @@ def test_data_client_created_privilege_record(self): ) test_data_client = DataClient(self.config) + # add provider record + provider_record = self.test_data_generator.put_default_provider_record_in_provider_table(value_overrides={ + 'privilegeJurisdictions': {} + }) response = test_data_client.create_provider_privileges( - compact='aslp', + compact=provider_record.compact, provider_id=DEFAULT_PROVIDER_ID, license_type='audiologist', jurisdiction_postal_abbreviations=['ky'], license_expiration_date=date.fromisoformat('2024-10-31'), - provider_record=self.test_data_generator.generate_default_provider(), + provider_record=provider_record, existing_privileges_for_license=[], compact_transaction_id='test_transaction_id', attestations=self.sample_privilege_attestations, @@ -244,8 +248,15 @@ def test_data_client_updates_privilege_records_for_specific_license_type(self): } ) - # Create the first privilege + provider_uuid = str(uuid4()) + # add provider record + self.test_data_generator.put_default_provider_record_in_provider_table( + value_overrides={ + 'providerId': provider_uuid, + 'privilegeJurisdictions': {} + }) + # Create the first privilege original_privilege = PrivilegeData.from_database_record( { 'pk': f'aslp#PROVIDER#{provider_uuid}', @@ -474,6 +485,13 @@ def test_data_client_handles_large_privilege_purchase(self): test_data_client = DataClient(self.config) provider_uuid = str(uuid4()) + # add provider record + self.test_data_generator.put_default_provider_record_in_provider_table( + value_overrides={ + 'providerId': provider_uuid, + 'privilegeJurisdictions': {} + }) + # use first 51 jurisdictions (will create 102 records - 51 privileges and 51 updates) jurisdictions = [jurisdiction for jurisdiction in self.config.jurisdictions[0:51]] @@ -487,7 +505,7 @@ def test_data_client_handles_large_privilege_purchase(self): 'providerId': provider_uuid, 'compact': 'aslp', 'jurisdiction': jurisdiction, - 'licenseJurisdiction': 'oh', + 'licenseJurisdiction': DEFAULT_LICENSE_JURISDICTION, 'licenseType': 'audiologist', 'privilegeId': f'AUD-{jurisdiction.upper()}-1', 'dateOfIssuance': datetime(2023, 11, 8, 23, 59, 59, tzinfo=UTC), From dcbd0f07b09f162c85e0dd71a0e7afa28ab19962 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 7 Apr 2026 11:45:47 -0500 Subject: [PATCH 15/15] linter --- .../common/tests/function/test_data_client.py | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py b/backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py index 69fd65375..3e4f6d575 100644 --- a/backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py +++ b/backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py @@ -6,7 +6,7 @@ from boto3.dynamodb.conditions import Key from cc_common.data_model.update_tier_enum import UpdateTierEnum from cc_common.exceptions import CCAwsServiceException, CCInvalidRequestException -from common_test.test_constants import DEFAULT_PROVIDER_ID, DEFAULT_LICENSE_JURISDICTION +from common_test.test_constants import DEFAULT_LICENSE_JURISDICTION, DEFAULT_PROVIDER_ID from moto import mock_aws from tests.function import TstFunction @@ -153,9 +153,9 @@ def test_data_client_created_privilege_record(self): test_data_client = DataClient(self.config) # add provider record - provider_record = self.test_data_generator.put_default_provider_record_in_provider_table(value_overrides={ - 'privilegeJurisdictions': {} - }) + provider_record = self.test_data_generator.put_default_provider_record_in_provider_table( + value_overrides={'privilegeJurisdictions': {}} + ) response = test_data_client.create_provider_privileges( compact=provider_record.compact, @@ -248,14 +248,11 @@ def test_data_client_updates_privilege_records_for_specific_license_type(self): } ) - provider_uuid = str(uuid4()) # add provider record self.test_data_generator.put_default_provider_record_in_provider_table( - value_overrides={ - 'providerId': provider_uuid, - 'privilegeJurisdictions': {} - }) + value_overrides={'providerId': provider_uuid, 'privilegeJurisdictions': {}} + ) # Create the first privilege original_privilege = PrivilegeData.from_database_record( { @@ -487,11 +484,8 @@ def test_data_client_handles_large_privilege_purchase(self): provider_uuid = str(uuid4()) # add provider record self.test_data_generator.put_default_provider_record_in_provider_table( - value_overrides={ - 'providerId': provider_uuid, - 'privilegeJurisdictions': {} - }) - + value_overrides={'providerId': provider_uuid, 'privilegeJurisdictions': {}} + ) # use first 51 jurisdictions (will create 102 records - 51 privileges and 51 updates) jurisdictions = [jurisdiction for jurisdiction in self.config.jurisdictions[0:51]]