Skip to content

Commit 1a7951c

Browse files
Fix/sprint 26 hotfixes (#914)
1 parent 121f381 commit 1a7951c

File tree

5 files changed

+188
-30
lines changed

5 files changed

+188
-30
lines changed

backend/compact-connect/lambdas/python/common/cc_common/data_model/compact_configuration_client.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,48 @@ def get_compact_configuration(self, compact: str) -> CompactConfigurationData:
9999

100100
def save_compact_configuration(self, compact_configuration: CompactConfigurationData) -> None:
101101
"""
102-
Save the compact configuration.
102+
Save the compact configuration, preserving existing fields like paymentProcessorPublicFields.
103+
If a record exists, it merges the new values with the existing record to preserve all fields.
103104
104105
:param compact_configuration: The compact configuration data
105106
"""
106107
logger.info('Saving compact configuration', compactAbbr=compact_configuration.compactAbbr)
107108

108-
serialized_compact = compact_configuration.serialize_to_database_record()
109-
110-
self.config.compact_configuration_table.put_item(Item=serialized_compact)
109+
try:
110+
existing_compact_config = self.get_compact_configuration(compact_configuration.compactAbbr)
111+
except CCNotFoundException:
112+
logger.info('Existing compact configuration not found.', compact=compact_configuration.compactAbbr)
113+
existing_compact_config = None
114+
115+
if existing_compact_config:
116+
# Record exists - merge with existing data to preserve fields like paymentProcessorPublicFields
117+
logger.info('Updating existing compact configuration record', compactAbbr=compact_configuration.compactAbbr)
118+
119+
# Load the existing record into a data class to get the existing data
120+
existing_data = existing_compact_config.to_dict()
121+
122+
# Get the new data
123+
new_data = compact_configuration.to_dict()
124+
125+
# Merge the data - new values override existing ones, but existing fields not in new_data are preserved
126+
merged_data = existing_data.copy()
127+
merged_data.update(new_data)
128+
129+
# Handle the special case where transactionFeeConfiguration should be removed
130+
# If the new configuration doesn't have transactionFeeConfiguration, remove it from merged data
131+
if 'transactionFeeConfiguration' not in new_data and 'transactionFeeConfiguration' in merged_data:
132+
del merged_data['transactionFeeConfiguration']
133+
134+
# Create a new CompactConfigurationData with the merged data
135+
merged_config = CompactConfigurationData.create_new(merged_data)
136+
final_serialized = merged_config.serialize_to_database_record()
137+
else:
138+
# First time creation - use the new data directly
139+
logger.info('Creating new compact configuration record', compactAbbr=compact_configuration.compactAbbr)
140+
final_serialized = compact_configuration.serialize_to_database_record()
141+
142+
# Use put_item to save the final record
143+
self.config.compact_configuration_table.put_item(Item=final_serialized)
111144

112145
def get_active_compact_jurisdictions(self, compact: str) -> list[dict]:
113146
"""

backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/common.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,10 +324,13 @@ class ClinicalPrivilegeActionCategory(CCEnum):
324324
https://www.npdb.hrsa.gov/software/CodeLists.pdf, Tables 41-45
325325
"""
326326

327+
NON_COMPLIANCE = 'Non-compliance With Requirements'
328+
CRIMINAL_CONVICTION = 'Criminal Conviction or Adjudication'
329+
CONFIDENTIALITY_VIOLATION = 'Confidentiality, Consent or Disclosure Violations'
330+
MISCONDUCT_ABUSE = 'Misconduct or Abuse'
327331
FRAUD = 'Fraud, Deception, or Misrepresentation'
328332
UNSAFE_PRACTICE = 'Unsafe Practice or Substandard Care'
329333
IMPROPER_SUPERVISION = 'Improper Supervision or Allowing Unlicensed Practice'
330-
IMPROPER_MEDICATION = 'Improper Prescribing, Dispensing, Administering Medication/Drug Violation'
331334
OTHER = 'Other'
332335

333336

backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/compact/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,7 @@ def compactSummaryReportNotificationEmails(self) -> list[str]:
130130
@property
131131
def licenseeRegistrationEnabled(self) -> bool:
132132
return self._data.get('licenseeRegistrationEnabled', False)
133+
134+
@property
135+
def paymentProcessorPublicFields(self) -> dict | None:
136+
return self._data.get('paymentProcessorPublicFields')

backend/compact-connect/lambdas/python/compact-configuration/handlers/compact_configuration.py

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -186,21 +186,44 @@ def _put_compact_configuration(event: dict, context: LambdaContext): # noqa: AR
186186
validated_data['compactName'] = compact_name
187187

188188
# Check if licenseeRegistrationEnabled is being changed from true to false
189-
if validated_data.get('licenseeRegistrationEnabled') is False:
190-
try:
191-
existing_config = config.compact_configuration_client.get_compact_configuration(compact=compact)
192-
if existing_config.licenseeRegistrationEnabled is True:
193-
logger.info(
194-
'attempt to disable licensee registration after it was enabled.',
195-
compact=compact,
196-
submitting_user_id=submitting_user_id,
197-
)
198-
raise CCInvalidRequestException(
199-
'Once licensee registration has been enabled, it cannot be disabled.'
200-
)
201-
except CCNotFoundException:
202-
# No existing configuration, so this is the first time setting this field
203-
logger.info('No existing configuration, so this is the first time setting this field', compact=compact)
189+
try:
190+
existing_config = config.compact_configuration_client.get_compact_configuration(compact=compact)
191+
if (
192+
existing_config.licenseeRegistrationEnabled is True
193+
and validated_data.get('licenseeRegistrationEnabled') is False
194+
):
195+
logger.info(
196+
'attempt to disable licensee registration after it was enabled.',
197+
compact=compact,
198+
submitting_user_id=submitting_user_id,
199+
)
200+
raise CCInvalidRequestException('Once licensee registration has been enabled, it cannot be disabled.')
201+
if (
202+
validated_data.get('licenseeRegistrationEnabled') is True
203+
and not existing_config.paymentProcessorPublicFields
204+
):
205+
logger.info(
206+
'licensee registration set to live without payment processor credentials.',
207+
compact=compact,
208+
submitting_user_id=submitting_user_id,
209+
)
210+
raise CCInvalidRequestException(
211+
'Authorize.net credentials not configured for compact. '
212+
'Please upload valid Authorize.net credentials.'
213+
)
214+
except CCNotFoundException as e:
215+
# No existing configuration, so this is the first time setting this field
216+
logger.info('No existing configuration, so this is the first time setting this field', compact=compact)
217+
if validated_data.get('licenseeRegistrationEnabled') is True:
218+
logger.info(
219+
'attempt to enable licensee registration without existing configuration.',
220+
compact=compact,
221+
submitting_user_id=submitting_user_id,
222+
)
223+
raise CCInvalidRequestException(
224+
'Authorize.net credentials need to be uploaded before the compact can be marked as live. '
225+
'Please submit all configuration values before setting the compact as live.'
226+
) from e
204227

205228
# Handle special case for transaction fee of 0
206229
if validated_data.get('transactionFeeConfiguration', {}).get('licenseeCharges', {}).get('chargeAmount') == 0:

backend/compact-connect/lambdas/python/compact-configuration/tests/function/test_compact_configuration.py

Lines changed: 105 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,50 @@ def _when_testing_get_compact_configuration_with_existing_compact_configuration(
194194
event['pathParameters']['compact'] = compact_config['compactAbbr']
195195
return event, compact_config
196196

197+
def _when_testing_put_compact_configuration_with_existing_configuration(
198+
self, set_payment_fields: bool = True, transaction_fee_zero: bool = False
199+
):
200+
from cc_common.utils import ResponseEncoder
201+
202+
value_overrides = {}
203+
if set_payment_fields:
204+
value_overrides.update(
205+
{'paymentProcessorPublicFields': {'publicClientKey': 'some-key', 'apiLoginId': 'some-login-id'}}
206+
)
207+
compact_config = self.test_data_generator.put_default_compact_configuration_in_configuration_table(
208+
value_overrides=value_overrides
209+
)
210+
211+
event = generate_test_event('PUT', COMPACT_CONFIGURATION_ENDPOINT_RESOURCE)
212+
event['pathParameters']['compact'] = compact_config.compactAbbr
213+
# add compact admin scope to the event
214+
event['requestContext']['authorizer']['claims']['scope'] = f'{compact_config.compactAbbr}/admin'
215+
event['requestContext']['authorizer']['claims']['sub'] = 'some-admin-id'
216+
217+
# we only allow the following values in the body
218+
event['body'] = json.dumps(
219+
{
220+
'compactCommissionFee': compact_config.compactCommissionFee,
221+
'licenseeRegistrationEnabled': compact_config.licenseeRegistrationEnabled,
222+
'compactOperationsTeamEmails': compact_config.compactOperationsTeamEmails,
223+
'compactAdverseActionsNotificationEmails': compact_config.compactAdverseActionsNotificationEmails,
224+
'compactSummaryReportNotificationEmails': compact_config.compactSummaryReportNotificationEmails,
225+
'transactionFeeConfiguration': compact_config.transactionFeeConfiguration
226+
if not transaction_fee_zero
227+
else {
228+
'licenseeCharges': {'chargeAmount': 0.00, 'chargeType': 'FLAT_FEE_PER_PRIVILEGE', 'active': True}
229+
},
230+
},
231+
cls=ResponseEncoder,
232+
)
233+
return event, compact_config
234+
197235
def _when_testing_put_compact_configuration(self, transaction_fee_zero: bool = False):
198236
from cc_common.utils import ResponseEncoder
199237

200-
compact_config = self.test_data_generator.generate_default_compact_configuration()
238+
compact_config = self.test_data_generator.generate_default_compact_configuration(
239+
value_overrides={'licenseeRegistrationEnabled': False}
240+
)
201241
event = generate_test_event('PUT', COMPACT_CONFIGURATION_ENDPOINT_RESOURCE)
202242
event['pathParameters']['compact'] = compact_config.compactAbbr
203243
# add compact admin scope to the event
@@ -302,7 +342,7 @@ def test_put_compact_configuration_rejects_state_admin_with_auth_error(self):
302342
self.assertEqual(403, response['statusCode'])
303343
self.assertIn('Access denied', json.loads(response['body'])['message'])
304344

305-
def test_put_compact_configuration_stores_compact_configuration(self):
345+
def test_put_compact_configuration_stores_new_compact_configuration(self):
306346
"""Test putting a compact configuration stores the compact configuration."""
307347
from cc_common.data_model.schema.compact import CompactConfigurationData
308348
from handlers.compact_configuration import compact_configuration_api_handler
@@ -322,6 +362,27 @@ def test_put_compact_configuration_stores_compact_configuration(self):
322362

323363
self.assertEqual(compact_config.to_dict(), stored_compact_data.to_dict())
324364

365+
def test_put_compact_configuration_preserves_payment_processor_fields_when_updating_compact_configuration(self):
366+
"""Test putting a compact configuration preserves existing fields not set by the request body."""
367+
from cc_common.data_model.schema.compact import CompactConfigurationData
368+
from handlers.compact_configuration import compact_configuration_api_handler
369+
370+
event, compact_config = self._when_testing_put_compact_configuration_with_existing_configuration()
371+
372+
response = compact_configuration_api_handler(event, self.mock_context)
373+
self.assertEqual(200, response['statusCode'], msg=json.loads(response['body']))
374+
375+
# load the record from the configuration table
376+
serialized_compact_config = compact_config.serialize_to_database_record()
377+
response = self.config.compact_configuration_table.get_item(
378+
Key={'pk': serialized_compact_config['pk'], 'sk': serialized_compact_config['sk']}
379+
)
380+
381+
stored_compact_data = CompactConfigurationData.from_database_record(response['Item'])
382+
# the compact_config variable has the 'paymentProcessorPublicFields' field, which we expect to also be
383+
# present in the stored_compact_data
384+
self.assertEqual(compact_config.to_dict(), stored_compact_data.to_dict())
385+
325386
def test_put_compact_configuration_removes_transaction_fee_when_zero(self):
326387
"""Test that when a transaction fee of 0 is provided, the transaction fee configuration is removed."""
327388
from cc_common.data_model.schema.compact import CompactConfigurationData
@@ -348,26 +409,60 @@ def test_put_compact_configuration_rejects_disabling_licensee_registration(self)
348409
from handlers.compact_configuration import compact_configuration_api_handler
349410

350411
# First, create a compact configuration with licenseeRegistrationEnabled=True
351-
event, original_config = self._when_testing_put_compact_configuration()
352-
# Set licenseeRegistrationEnabled to True in the request body
412+
event, _ = self._when_testing_put_compact_configuration_with_existing_configuration()
413+
414+
# Now attempt to update with licenseeRegistrationEnabled=False
353415
body = json.loads(event['body'])
354-
body['licenseeRegistrationEnabled'] = True
416+
body['licenseeRegistrationEnabled'] = False
355417
event['body'] = json.dumps(body)
356418

357-
# Submit the configuration
358-
compact_configuration_api_handler(event, self.mock_context)
419+
# Should be rejected with a 400 error
420+
response = compact_configuration_api_handler(event, self.mock_context)
421+
self.assertEqual(400, response['statusCode'])
422+
response_body = json.loads(response['body'])
423+
self.assertIn('Once licensee registration has been enabled, it cannot be disabled', response_body['message'])
359424

360-
# Now attempt to update with licenseeRegistrationEnabled=False
425+
def test_put_compact_configuration_rejects_enabling_licensee_registration_without_payment_credentials(self):
426+
"""Test that a compact configuration update is rejected if trying to enable licensee registration without payment processor credentials."""
427+
from handlers.compact_configuration import compact_configuration_api_handler
428+
429+
# Attempt to enable licensee registration without any existing configuration (no payment credentials)
361430
event, _ = self._when_testing_put_compact_configuration()
362431
body = json.loads(event['body'])
363-
body['licenseeRegistrationEnabled'] = False
432+
body['licenseeRegistrationEnabled'] = True
364433
event['body'] = json.dumps(body)
365434

366435
# Should be rejected with a 400 error
367436
response = compact_configuration_api_handler(event, self.mock_context)
368437
self.assertEqual(400, response['statusCode'])
369438
response_body = json.loads(response['body'])
370-
self.assertIn('Once licensee registration has been enabled, it cannot be disabled', response_body['message'])
439+
self.assertIn(
440+
'Authorize.net credentials need to be uploaded before the compact can be marked as live.',
441+
response_body['message'],
442+
)
443+
444+
def test_put_compact_configuration_rejects_enabling_licensee_registration_with_existing_config_without_payment_credentials(
445+
self,
446+
):
447+
"""Test that a compact configuration update is rejected if trying to enable licensee registration when existing config has no payment credentials."""
448+
from handlers.compact_configuration import compact_configuration_api_handler
449+
450+
# First, create a basic compact configuration without payment credentials
451+
event, _ = self._when_testing_put_compact_configuration_with_existing_configuration(set_payment_fields=False)
452+
453+
# Now attempt to enable licensee registration without payment credentials
454+
body = json.loads(event['body'])
455+
body['licenseeRegistrationEnabled'] = True
456+
event['body'] = json.dumps(body)
457+
458+
# Should be rejected with a 400 error
459+
response = compact_configuration_api_handler(event, self.mock_context)
460+
self.assertEqual(400, response['statusCode'])
461+
response_body = json.loads(response['body'])
462+
self.assertIn(
463+
'Authorize.net credentials not configured for compact. Please upload valid Authorize.net credentials.',
464+
response_body['message'],
465+
)
371466

372467

373468
TEST_MILITARY_RATE = Decimal('40.00')

0 commit comments

Comments
 (0)