Skip to content

Commit aa655f6

Browse files
Filter privilege options based on states that are live
1 parent b47d03a commit aa655f6

File tree

3 files changed

+197
-26
lines changed

3 files changed

+197
-26
lines changed

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

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
from boto3.dynamodb.conditions import Attr, Key
1+
from boto3.dynamodb.conditions import Key
22

33
from cc_common.config import _Config, logger
4-
from cc_common.data_model.query_paginator import paginated_query
54
from cc_common.data_model.schema.attestation import AttestationRecordSchema
65
from cc_common.data_model.schema.compact import CompactConfigurationData
6+
from cc_common.data_model.schema.compact.common import COMPACT_TYPE
77
from cc_common.data_model.schema.compact.record import CompactRecordSchema
88
from cc_common.data_model.schema.jurisdiction import JurisdictionConfigurationData
99
from cc_common.data_model.schema.jurisdiction.record import JurisdictionRecordSchema
@@ -242,18 +242,88 @@ def _ensure_jurisdiction_in_configured_states_if_registration_enabled(
242242
jurisdiction=jurisdiction_config.postalAbbreviation,
243243
)
244244

245-
@paginated_query
246245
@logger_inject_kwargs(logger, 'compact')
247-
def get_privilege_purchase_options(self, *, compact: str, dynamo_pagination: dict):
246+
def get_privilege_purchase_options(self, *, compact: str):
248247
logger.info('Getting privilege purchase options for compact')
249248

250-
return self.config.compact_configuration_table.query(
251-
Select='ALL_ATTRIBUTES',
252-
KeyConditionExpression=Key('pk').eq(f'{compact}#CONFIGURATION'),
253-
FilterExpression=Attr('licenseeRegistrationEnabled').eq(True),
254-
**dynamo_pagination,
249+
# Get all compact configurations (both compact and jurisdiction records)
250+
# Use pagination to ensure we get all records
251+
all_items = []
252+
query_params = {
253+
'Select': 'ALL_ATTRIBUTES',
254+
'KeyConditionExpression': Key('pk').eq(f'{compact}#CONFIGURATION'),
255+
}
256+
257+
while True:
258+
response = self.config.compact_configuration_table.query(**query_params)
259+
all_items.extend(response.get('Items', []))
260+
261+
# Check if there are more records to fetch
262+
last_evaluated_key = response.get('LastEvaluatedKey')
263+
if not last_evaluated_key:
264+
break
265+
266+
# Set up for next page
267+
query_params['ExclusiveStartKey'] = last_evaluated_key
268+
269+
logger.info(
270+
'Retrieved all configuration records',
271+
total_items=len(all_items),
255272
)
256273

274+
# Get the compact configuration from the response items to access configuredStates
275+
compact_config_item = next((item for item in all_items if item['type'] == COMPACT_TYPE), None)
276+
277+
if compact_config_item and compact_config_item.get('configuredStates'):
278+
live_jurisdictions = {
279+
state['postalAbbreviation'] for state in compact_config_item['configuredStates'] if state.get('isLive')
280+
}
281+
logger.info(
282+
'Filtering privilege purchase options by live jurisdictions',
283+
live_jurisdictions=list(live_jurisdictions),
284+
)
285+
else:
286+
message = (
287+
'Compact configuration not found or has no configuredStates when filtering privilege purchase options'
288+
)
289+
logger.error(
290+
message,
291+
compact_config_found=compact_config_item is not None,
292+
configured_states=compact_config_item.get('configuredStates') if compact_config_item else None,
293+
)
294+
# in this case, there is nothing to return, so we return an empty list, and let the caller decide to raise
295+
# an exception or not.
296+
return {'items': []}
297+
298+
# Filter the response items to only include live jurisdictions
299+
filtered_items = []
300+
for item in all_items:
301+
# Always include compact configuration records
302+
if item.get('type') == COMPACT_TYPE:
303+
filtered_items.append(item)
304+
# Only include jurisdiction records that are live
305+
elif item.get('type') == 'jurisdiction':
306+
jurisdiction_postal = item.get('postalAbbreviation', '').lower()
307+
if jurisdiction_postal in live_jurisdictions:
308+
filtered_items.append(item)
309+
else:
310+
logger.debug(
311+
'Filtering out non-live jurisdiction from privilege purchase options',
312+
compact=compact,
313+
jurisdiction=jurisdiction_postal,
314+
is_live=False,
315+
)
316+
317+
# Return in the expected format for backward compatibility
318+
return {
319+
'items': filtered_items,
320+
'pagination': {
321+
'pageSize': len(filtered_items),
322+
'prevLastKey': None,
323+
'lastKey': None,
324+
},
325+
}
326+
257327
def set_compact_authorize_net_public_values(self, compact: str, api_login_id: str, public_client_key: str) -> None:
258328
"""
259329
Set the payment processor public fields (apiLoginId and publicClientKey) for a compact's configuration.

backend/compact-connect/lambdas/python/purchases/handlers/privileges.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,14 @@ def get_purchase_privilege_options(event: dict, context: LambdaContext): # noqa
7878

7979
options_response = config.compact_configuration_client.get_privilege_purchase_options(
8080
compact=compact,
81-
pagination=event.get('queryStringParameters', {}),
8281
)
8382

83+
# In theory, this should never happen as a practitioner can only call this endpoint if at least one state
84+
# was enabled for users to register in, but we still check for it here so we can be alerted if it ever occurs.
85+
if not options_response.get('items'):
86+
logger.error('No privilege options returned for compact.', compact=compact)
87+
raise CCInternalException('No privilege options returned for compact.')
88+
8489
# we need to filter out contact information from the response, which is not needed by the client
8590
serlialized_options = []
8691
for item in options_response['items']:

backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_privilege_options.py

Lines changed: 112 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22

3+
from cc_common.exceptions import CCInternalException
34
from moto import mock_aws
45

56
from .. import TstFunction
@@ -16,10 +17,13 @@ class TestGetPurchasePrivilegeOptions(TstFunction):
1617
def _when_testing_provider_user_event_with_custom_claims(self, test_compact=TEST_COMPACT):
1718
self.test_data_generator.put_default_compact_configuration_in_configuration_table(
1819
value_overrides={
20+
'configuredStates': [
21+
{'postalAbbreviation': 'ky', 'isLive': True} # Make Kentucky live
22+
],
1923
'paymentProcessorPublicFields': {
2024
'publicClientKey': MOCK_PUBLIC_CLIENT_KEY,
2125
'apiLoginId': MOCK_API_LOGIN_ID,
22-
}
26+
},
2327
}
2428
)
2529
self.test_data_generator.put_default_jurisdiction_configuration_in_configuration_table()
@@ -36,6 +40,20 @@ def test_get_purchase_privilege_options_returns_expected_jurisdiction_option(sel
3640

3741
event = self._when_testing_provider_user_event_with_custom_claims()
3842

43+
# Set up compact configuration with mixed live statuses
44+
self.test_data_generator.put_default_compact_configuration_in_configuration_table(
45+
value_overrides={
46+
'configuredStates': [
47+
{'postalAbbreviation': 'ky', 'isLive': True}, # Live
48+
{'postalAbbreviation': 'oh', 'isLive': False}, # Not live
49+
],
50+
'paymentProcessorPublicFields': {
51+
'publicClientKey': MOCK_PUBLIC_CLIENT_KEY,
52+
'apiLoginId': MOCK_API_LOGIN_ID,
53+
},
54+
}
55+
)
56+
3957
resp = get_purchase_privilege_options(event, self.mock_context)
4058

4159
self.assertEqual(200, resp['statusCode'])
@@ -111,28 +129,76 @@ def test_get_purchase_privilege_options_returns_400_if_api_call_made_without_pro
111129

112130
self.assertEqual(400, resp['statusCode'])
113131

114-
def test_get_purchase_privilege_options_returns_empty_list_if_user_compact_do_not_match_any_option_in_db(self):
132+
def test_get_purchase_privilege_options_filters_out_jurisdictions_with_licensee_registration_disabled(self):
115133
from handlers.privileges import get_purchase_privilege_options
116134

117-
event = self._when_testing_provider_user_event_with_custom_claims(test_compact='some-compact')
135+
event = self._when_testing_provider_user_event_with_custom_claims()
136+
137+
# Set up compact configuration. In this case, because ohio has not elected to go live, it does not show up
138+
# in the list of configured states
139+
self.test_data_generator.put_default_compact_configuration_in_configuration_table(
140+
value_overrides={
141+
'configuredStates': [
142+
{'postalAbbreviation': 'ky', 'isLive': True} # Make Kentucky live
143+
],
144+
'paymentProcessorPublicFields': {
145+
'publicClientKey': MOCK_PUBLIC_CLIENT_KEY,
146+
'apiLoginId': MOCK_API_LOGIN_ID,
147+
},
148+
}
149+
)
150+
151+
# Create jurisdiction with licenseeRegistrationEnabled = True
152+
self.test_data_generator.put_default_jurisdiction_configuration_in_configuration_table(
153+
value_overrides={
154+
'postalAbbreviation': 'ky',
155+
'jurisdictionName': 'Kentucky',
156+
'licenseeRegistrationEnabled': True,
157+
}
158+
)
159+
160+
# Create jurisdiction with licenseeRegistrationEnabled = False
161+
self.test_data_generator.put_default_jurisdiction_configuration_in_configuration_table(
162+
value_overrides={
163+
'postalAbbreviation': 'oh',
164+
'jurisdictionName': 'Ohio',
165+
'licenseeRegistrationEnabled': False,
166+
}
167+
)
168+
169+
self._load_provider_data()
118170

119171
resp = get_purchase_privilege_options(event, self.mock_context)
120172

121173
self.assertEqual(200, resp['statusCode'])
122174
privilege_options = json.loads(resp['body'])
123175

124-
self.assertEqual([], privilege_options['items'])
176+
# ensure the compact and privilege were returned
177+
self.assertEqual(2, len(privilege_options['items']))
125178

126-
def test_get_purchase_privilege_options_filters_out_jurisdictions_with_licensee_registration_disabled(self):
179+
# Filter to only jurisdiction options
180+
jurisdiction_options = [option for option in privilege_options['items'] if option['type'] == 'jurisdiction']
181+
182+
# Should only return the jurisdiction with licenseeRegistrationEnabled = True
183+
self.assertEqual(1, len(jurisdiction_options))
184+
returned_jurisdiction = jurisdiction_options[0]
185+
self.assertEqual('ky', returned_jurisdiction['postalAbbreviation'])
186+
self.assertEqual('Kentucky', returned_jurisdiction['jurisdictionName'])
187+
188+
def test_get_purchase_privilege_options_raises_exception_if_no_live_configured_states(self):
189+
"""Test that jurisdictions not in configuredStates are filtered out."""
127190
from handlers.privileges import get_purchase_privilege_options
128191

129-
# Set up compact configuration
192+
event = self._when_testing_provider_user_event_with_custom_claims()
193+
194+
# Set up compact configuration with empty configuredStates
130195
self.test_data_generator.put_default_compact_configuration_in_configuration_table(
131196
value_overrides={
197+
'configuredStates': [], # Empty configuredStates
132198
'paymentProcessorPublicFields': {
133199
'publicClientKey': MOCK_PUBLIC_CLIENT_KEY,
134200
'apiLoginId': MOCK_API_LOGIN_ID,
135-
}
201+
},
136202
}
137203
)
138204

@@ -145,32 +211,62 @@ def test_get_purchase_privilege_options_filters_out_jurisdictions_with_licensee_
145211
}
146212
)
147213

148-
# Create jurisdiction with licenseeRegistrationEnabled = False
214+
with self.assertRaises(CCInternalException):
215+
get_purchase_privilege_options(event, self.mock_context)
216+
217+
def test_get_purchase_privilege_options_includes_live_jurisdictions_in_configured_states(self):
218+
"""Test that only jurisdictions with isLive=true are included."""
219+
from handlers.privileges import get_purchase_privilege_options
220+
221+
event = self._when_testing_provider_user_event_with_custom_claims()
222+
223+
# Set up compact configuration with mixed live statuses
224+
self.test_data_generator.put_default_compact_configuration_in_configuration_table(
225+
value_overrides={
226+
'configuredStates': [
227+
{'postalAbbreviation': 'ky', 'isLive': True}, # Live
228+
{'postalAbbreviation': 'oh', 'isLive': False}, # Not live
229+
],
230+
'paymentProcessorPublicFields': {
231+
'publicClientKey': MOCK_PUBLIC_CLIENT_KEY,
232+
'apiLoginId': MOCK_API_LOGIN_ID,
233+
},
234+
}
235+
)
236+
237+
# Create both jurisdictions with licenseeRegistrationEnabled = True
238+
self.test_data_generator.put_default_jurisdiction_configuration_in_configuration_table(
239+
value_overrides={
240+
'postalAbbreviation': 'ky',
241+
'jurisdictionName': 'Kentucky',
242+
'licenseeRegistrationEnabled': True,
243+
}
244+
)
245+
149246
self.test_data_generator.put_default_jurisdiction_configuration_in_configuration_table(
150247
value_overrides={
151248
'postalAbbreviation': 'oh',
152249
'jurisdictionName': 'Ohio',
153-
'licenseeRegistrationEnabled': False,
250+
'licenseeRegistrationEnabled': True,
154251
}
155252
)
156253

157-
self._load_provider_data()
158-
159-
event = self._when_testing_provider_user_event_with_custom_claims()
160-
161254
resp = get_purchase_privilege_options(event, self.mock_context)
162255

163256
self.assertEqual(200, resp['statusCode'])
164257
privilege_options = json.loads(resp['body'])
165258

166-
# ensure the compact and privilege were returned
259+
# Should return compact option + 1 live jurisdiction
167260
self.assertEqual(2, len(privilege_options['items']))
168261

169-
# Filter to only jurisdiction options
262+
# Verify compact option and one jurisdiction option are returned
263+
compact_options = [option for option in privilege_options['items'] if option['type'] == 'compact']
170264
jurisdiction_options = [option for option in privilege_options['items'] if option['type'] == 'jurisdiction']
171265

172-
# Should only return the jurisdiction with licenseeRegistrationEnabled = True
266+
self.assertEqual(1, len(compact_options))
173267
self.assertEqual(1, len(jurisdiction_options))
268+
269+
# Verify only the live jurisdiction (Kentucky) is returned
174270
returned_jurisdiction = jurisdiction_options[0]
175271
self.assertEqual('ky', returned_jurisdiction['postalAbbreviation'])
176272
self.assertEqual('Kentucky', returned_jurisdiction['jurisdictionName'])

0 commit comments

Comments
 (0)