Skip to content

Commit 137ac54

Browse files
Feat/prevent repeated ssns (#1187)
1 parent 62139fd commit 137ac54

17 files changed

Lines changed: 238 additions & 29 deletions

File tree

.github/workflows/check-compact-connect.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,11 @@ jobs:
101101

102102
- name: Install dev dependencies
103103
run: "pip install -r backend/compact-connect/requirements-dev.txt"
104-
104+
# Note we are currently pinning the pip version to deal with compatibility issues released with pip 25.3
105+
# see https://stackoverflow.com/a/79802727 If this issues is addressed in a later version, we can remove the
106+
# extra pip install command so we stop pinning the pip version
105107
- name: Install all Python dependencies
106-
run: "cd backend/compact-connect; bin/sync_deps.sh"
108+
run: "cd backend/compact-connect; pip install -U 'pip<25.3'; bin/sync_deps.sh"
107109

108110
- name: Test backend
109111
run: "cd backend/compact-connect; bin/run_tests.sh -l all -no"

backend/common-cdk/common_constructs/stack.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ def __init__(self, *args, environment_context: dict, environment_name: str, **kw
9797

9898
self.environment_context = environment_context
9999
self.environment_name = environment_name
100+
# We only set the API_BASE_URL common env var if the API_DOMAIN_NAME is set
101+
# The API_BASE_URL is used by the feature flag client to call the flag check endpoint
102+
if self.api_domain_name:
103+
self.common_env_vars.update({'API_BASE_URL': f'https://{self.api_domain_name}'})
100104

101105
@cached_property
102106
def hosted_zone(self) -> IHostedZone | None:

backend/compact-connect/docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Export your license data to a CSV file, formatted as follows:
3131
- String lengths are enforced - exceeding them will cause validation errors
3232
- Some fields have a set list of allowed values. For those fields, make sure to enter the value exactly, including
3333
spacing and capitalization
34+
- SSNs must be unique within a single CSV upload file. Do not include multiple rows with the same `ssn` in one file. If duplicate SSNs are sent within the same file, the first row will be processed, but all other duplicate rows will be rejected.
3435

3536
#### Field Descriptions
3637

backend/compact-connect/docs/it_staff_onboarding_instructions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ For your convenience, use of this feature is included in the [Postman Collection
240240
- `licenseType` must match exactly with one of the valid types for the specified compact
241241
- All date fields must use the `YYYY-MM-DD` format
242242
- The API does not accept `null` values. For optional fields with no value, omit the field or leave it empty in CSV.
243+
- For CSV uploads, SSNs must be unique within a single file. Do not include multiple rows with the same `ssn` in one upload. If duplicate SSNs are sent within the same file, the first row will be processed, but all other duplicate rows will be rejected.
244+
- For JSON uploads, SSNs must be unique within a single request payload (array). Do not include duplicate `ssn` values in the same batch. Attempting to do so will cause the entire request to be rejected.
243245

244246
## Common Upload Strategies: JSON vs CSV
245247

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,9 +1428,9 @@ def encumber_privilege(self, adverse_action: AdverseActionData) -> None:
14281428

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

1433-
if is_feature_enabled('encumbrance-multi-category-flag'):
1433+
if is_feature_enabled(FeatureFlagEnum.ENCUMBRANCE_MULTI_CATEGORY_FLAG):
14341434
encumbrance_details = {
14351435
'clinicalPrivilegeActionCategories': adverse_action.clinicalPrivilegeActionCategories,
14361436
'adverseActionId': adverse_action.adverseActionId,
@@ -2674,9 +2674,9 @@ def encumber_home_jurisdiction_license_privileges(
26742674
'Found privileges to encumber', privilege_count=len(unencumbered_privileges_associated_with_license)
26752675
)
26762676
# TODO - remove the flag as part of https://github.com/csg-org/CompactConnect/issues/1136 # noqa: FIX002
2677-
from cc_common.feature_flag_client import is_feature_enabled
2677+
from cc_common.feature_flag_client import FeatureFlagEnum, is_feature_enabled
26782678

2679-
if is_feature_enabled('encumbrance-multi-category-flag'):
2679+
if is_feature_enabled(FeatureFlagEnum.ENCUMBRANCE_MULTI_CATEGORY_FLAG):
26802680
encumbrance_details = {
26812681
'clinicalPrivilegeActionCategories': adverse_action.clinicalPrivilegeActionCategories,
26822682
'licenseJurisdiction': jurisdiction,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,9 +375,9 @@ def construct_simplified_privilege_history_object(
375375
):
376376
# TODO - remove the flag as part of https://github.com/csg-org/CompactConnect/issues/1136 # noqa: FIX002
377377
# as well as check for deprecated field
378-
from cc_common.feature_flag_client import is_feature_enabled
378+
from cc_common.feature_flag_client import FeatureFlagEnum, is_feature_enabled
379379

380-
if is_feature_enabled('encumbrance-multi-category-flag'):
380+
if is_feature_enabled(FeatureFlagEnum.ENCUMBRANCE_MULTI_CATEGORY_FLAG):
381381
if 'clinicalPrivilegeActionCategory' in event['encumbranceDetails']:
382382
event['note'] = event['encumbranceDetails'].get('clinicalPrivilegeActionCategory')
383383
else:

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,6 @@ class UpdateCategory(CCEnum):
288288
DEACTIVATION = 'deactivation'
289289
EXPIRATION = 'expiration'
290290
ISSUANCE = 'issuance'
291-
OTHER = 'other'
292291
RENEWAL = 'renewal'
293292
ENCUMBRANCE = 'encumbrance'
294293
HOME_JURISDICTION_CHANGE = 'homeJurisdictionChange'
@@ -297,6 +296,9 @@ class UpdateCategory(CCEnum):
297296
# this is specific to privileges that are deactivated due to a state license deactivation
298297
LICENSE_DEACTIVATION = 'licenseDeactivation'
299298
EMAIL_CHANGE = 'emailChange'
299+
# NOTE: this value should explicitly be used for license upload updates, not anywhere else
300+
# it is referenced in the event that an invalid license upload needs to be reverted.
301+
LICENSE_UPLOAD_UPDATE_OTHER = 'other'
300302

301303

302304
class ActiveInactiveStatus(CCEnum):

backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import requests
1212

1313
from cc_common.config import config, logger
14+
from cc_common.feature_flag_enum import FeatureFlagEnum
1415

1516

1617
@dataclass
@@ -43,14 +44,16 @@ def to_dict(self) -> dict[str, Any]:
4344
return result
4445

4546

46-
def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None, fail_default: bool = False) -> bool:
47+
def is_feature_enabled(
48+
flag_name: FeatureFlagEnum, context: FeatureFlagContext | None = None, fail_default: bool = False
49+
) -> bool:
4750
"""
4851
Check if a feature flag is enabled.
4952
5053
This function calls the internal feature flag API endpoint to determine
5154
if a feature flag is enabled for the given context.
5255
53-
:param flag_name: The name of the feature flag to check
56+
:param flag_name: The name of the feature flag to check.
5457
:param context: Optional FeatureFlagContext for feature flag evaluation
5558
:param fail_default: If True, return True on errors; if False, return False on errors (default: False)
5659
:return: True if the feature flag is enabled, False otherwise (or fail_default value on error)
@@ -76,6 +79,7 @@ def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None
7679
):
7780
"""
7881
try:
82+
logger.info("checking status of feature flag", flag_name=flag_name)
7983
api_base_url = _get_api_base_url()
8084
endpoint_url = f'{api_base_url}/v1/flags/{flag_name}/check'
8185

@@ -103,7 +107,8 @@ def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None
103107
# Invalid response format - return fail_default value
104108
return fail_default
105109

106-
return bool(response_data['enabled'])
110+
logger.info('Checked flag status successfully', flag_name=flag_name, enabled=response_data['enabled'])
111+
return response_data['enabled']
107112

108113
# We catch all exceptions to prevent a feature flag issue causing the system from operating
109114
except Exception as e: # noqa: BLE001
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from enum import StrEnum
2+
3+
4+
class FeatureFlagEnum(StrEnum):
5+
"""
6+
Central source for all feature flags currently referenced in the python code of the project.
7+
Flags should be defined here when first added, and removed when the flag
8+
is no longer in use.
9+
"""
10+
11+
# flag used by internal testing
12+
TEST_FLAG = 'test-flag'
13+
# runtime flags
14+
ENCUMBRANCE_MULTI_CATEGORY_FLAG = 'encumbrance-multi-category-flag'
15+
DUPLICATE_SSN_UPLOAD_CHECK_FLAG = 'duplicate-ssn-upload-check-flag'

backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from unittest.mock import MagicMock, patch
22

3+
from cc_common.feature_flag_enum import FeatureFlagEnum
4+
35
from tests import TstLambdas
46

57

@@ -13,7 +15,7 @@ def test_is_feature_enabled_returns_true_when_flag_enabled(self):
1315
mock_response.json.return_value = {'enabled': True}
1416

1517
with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response) as mock_post:
16-
result = is_feature_enabled('test-flag')
18+
result = is_feature_enabled(FeatureFlagEnum.TEST_FLAG)
1719

1820
# Verify the result
1921
self.assertTrue(result)
@@ -35,7 +37,7 @@ def test_is_feature_enabled_returns_false_when_flag_disabled(self):
3537
mock_response.json.return_value = {'enabled': False}
3638

3739
with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response):
38-
result = is_feature_enabled('test-flag')
40+
result = is_feature_enabled(FeatureFlagEnum.TEST_FLAG)
3941

4042
# Verify the result
4143
self.assertFalse(result)
@@ -51,7 +53,7 @@ def test_is_feature_enabled_with_context(self):
5153
context = FeatureFlagContext(user_id='user123', custom_attributes={'licenseType': 'lpc'})
5254

5355
with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response) as mock_post:
54-
result = is_feature_enabled('test-flag', context=context)
56+
result = is_feature_enabled(FeatureFlagEnum.TEST_FLAG, context=context)
5557

5658
# Verify the result
5759
self.assertTrue(result)
@@ -71,7 +73,7 @@ def test_is_feature_enabled_fail_closed_on_timeout(self):
7173
from cc_common.feature_flag_client import is_feature_enabled
7274

7375
with patch('cc_common.feature_flag_client.requests.post', side_effect=Exception('Timeout')):
74-
result = is_feature_enabled('test-flag', fail_default=False)
76+
result = is_feature_enabled(FeatureFlagEnum.TEST_FLAG, fail_default=False)
7577

7678
# Verify it fails closed (returns False)
7779
self.assertFalse(result)
@@ -81,7 +83,7 @@ def test_is_feature_enabled_fail_open_on_timeout(self):
8183
from cc_common.feature_flag_client import is_feature_enabled
8284

8385
with patch('cc_common.feature_flag_client.requests.post', side_effect=Exception('Timeout')):
84-
result = is_feature_enabled('test-flag', fail_default=True)
86+
result = is_feature_enabled(FeatureFlagEnum.TEST_FLAG, fail_default=True)
8587

8688
# Verify it fails open (returns True)
8789
self.assertTrue(result)
@@ -95,7 +97,7 @@ def test_is_feature_enabled_fail_closed_on_http_error(self):
9597
mock_response.raise_for_status.side_effect = Exception('500 Server Error')
9698

9799
with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response):
98-
result = is_feature_enabled('test-flag', fail_default=False)
100+
result = is_feature_enabled(FeatureFlagEnum.TEST_FLAG, fail_default=False)
99101

100102
# Verify it fails closed (returns False)
101103
self.assertFalse(result)
@@ -109,7 +111,7 @@ def test_is_feature_enabled_fail_open_on_http_error(self):
109111
mock_response.raise_for_status.side_effect = Exception('500 Server Error')
110112

111113
with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response):
112-
result = is_feature_enabled('test-flag', fail_default=True)
114+
result = is_feature_enabled(FeatureFlagEnum.TEST_FLAG, fail_default=True)
113115

114116
# Verify it fails open (returns True)
115117
self.assertTrue(result)
@@ -124,7 +126,7 @@ def test_is_feature_enabled_fail_closed_on_invalid_response(self):
124126
mock_response.raise_for_status = MagicMock()
125127

126128
with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response):
127-
result = is_feature_enabled('test-flag', fail_default=False)
129+
result = is_feature_enabled(FeatureFlagEnum.TEST_FLAG, fail_default=False)
128130

129131
# Verify it fails closed (returns False)
130132
self.assertFalse(result)
@@ -139,7 +141,7 @@ def test_is_feature_enabled_fail_open_on_invalid_response(self):
139141
mock_response.raise_for_status = MagicMock()
140142

141143
with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response):
142-
result = is_feature_enabled('test-flag', fail_default=True)
144+
result = is_feature_enabled(FeatureFlagEnum.TEST_FLAG, fail_default=True)
143145

144146
# Verify it fails open (returns True)
145147
self.assertTrue(result)
@@ -154,7 +156,7 @@ def test_is_feature_enabled_fail_closed_on_json_parse_error(self):
154156
mock_response.raise_for_status = MagicMock()
155157

156158
with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response):
157-
result = is_feature_enabled('test-flag', fail_default=False)
159+
result = is_feature_enabled(FeatureFlagEnum.TEST_FLAG, fail_default=False)
158160

159161
# Verify it fails closed (returns False)
160162
self.assertFalse(result)
@@ -168,7 +170,7 @@ def test_is_feature_enabled_fail_open_on_json_parse_error(self):
168170
mock_response.json.side_effect = ValueError('Invalid JSON')
169171

170172
with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response):
171-
result = is_feature_enabled('test-flag', fail_default=True)
173+
result = is_feature_enabled(FeatureFlagEnum.TEST_FLAG, fail_default=True)
172174

173175
# Verify it fails open (returns True)
174176
self.assertTrue(result)
@@ -220,7 +222,7 @@ def test_is_feature_enabled_with_context_user_id_only(self):
220222
context = FeatureFlagContext(user_id='user789')
221223

222224
with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response) as mock_post:
223-
result = is_feature_enabled('test-flag', context=context)
225+
result = is_feature_enabled(FeatureFlagEnum.TEST_FLAG, context=context)
224226

225227
# Verify the result
226228
self.assertTrue(result)

0 commit comments

Comments
 (0)