Skip to content

Determine if license is eligible/ineligible in the public search list view response#1558

Open
landonshumway-ia wants to merge 23 commits into
csg-org:mainfrom
InspiringApps:feat/cosm-license-eligibility
Open

Determine if license is eligible/ineligible in the public search list view response#1558
landonshumway-ia wants to merge 23 commits into
csg-org:mainfrom
InspiringApps:feat/cosm-license-eligibility

Conversation

@landonshumway-ia
Copy link
Copy Markdown
Collaborator

@landonshumway-ia landonshumway-ia commented May 11, 2026

For the cosmetology public license list view, the frontend will be adding a column that shows if the license for that row is eligible or ineligible. This updates the API for the respective search endpoint to return a new field 'licenseEligibility', which the frontend will use to populate that column for the list view.

In addition, the Cosmetology directors have determined that for the public search, we only want the most recently issued/renewed license to be searchable/visible in the public search. To support this, we have added a new bool field to the documents that are indexed in OpenSearch, 'mostRecentLicenseForType', which is set to true for licenses that are the most recent license for a practitioner for a specific license type, and false for any other previous licenses. This allows us to exclude the older licenses when performing public searches.

Testing List

  • yarn test:unit:all should run without errors or warnings
  • yarn serve should run without errors or warnings
  • yarn build should run without errors or warnings
  • For API configuration changes: CDK tests added/updated in backend/compact-connect/tests/unit/test_api.py
  • For API endpoint changes: OpenAPI spec updated to show latest endpoint configuration run compact-connect/bin/download_oas30.py
  • Code review

Closes #1479

Summary by CodeRabbit

Release Notes

  • New Features

    • Added license eligibility indicators to public provider search results, distinguishing between eligible and ineligible licenses
    • Implemented most-recent license filtering in public search results—only the newest license per type is displayed
  • Bug Fixes

    • Improved license expiration status handling and tracking of license adverse actions
  • Tests

    • Expanded public search smoke test coverage with additional eligibility scenarios
  • Chores

    • Updated multiple dependencies including AWS SDK libraries and security packages
    • Updated OpenAPI specifications and documentation to reflect new license eligibility fields

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Important

Review skipped

This PR was authored by the user configured for CodeRabbit reviews. CodeRabbit does not review PRs authored by this user. It's recommended to use a dedicated user account to post CodeRabbit review feedback.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 36c27c47-0136-487a-8f94-850ae249d215

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements public license eligibility determination in search results by filtering to the most-recent license per type and computing eligibility based on adverse actions, expiration status, and jurisdiction participation. Changes span provider record logic, OpenSearch document generation and querying, API schemas, comprehensive test coverage across unit/function/smoke layers, and coordinated dependency version bumps.

Changes

Public License Eligibility & Filtering

Layer / File(s) Summary
License Selection Logic
backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py, backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py
Module-level _license_sort_key() and new ProviderUserRecords methods _sort_licenses_by_most_recent() and find_most_recent_licenses_for_each_license_type() group and select most-recent per license type; generate_api_response_object(is_public_response=False) uses this for public-only filtering.
OpenSearch Document Generation
backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py
Refactored to precompute provider-level adverseActions once and attach to every document; precomputed most-recent-per-type set; marked each document with mostRecentLicenseForType boolean; conditionally include privileges only for most-recent licenses.
License Expiration & Compact Eligibility
backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/license/api.py
LicenseExpirationStatusMixin.correct_expired_license_status() now marks both licenseStatus (inactive) and compactEligibility (ineligible) when license is expired.
OpenSearch Index Mapping
backend/cosmetology-app/lambdas/python/search/opensearch_client.py
Added mostRecentLicenseForType boolean field to license properties; added top-level adverseActions nested mapping to provider documents.
Public Search Eligibility Engine
backend/cosmetology-app/lambdas/python/search/handlers/public_search.py
New _determine_license_eligibility() helper loads provider document, validates schema, checks for unlifted adverse actions (license or privilege), and returns eligibility status; _public_query_licenses() uses this to set per-license licenseEligibility; _build_public_license_search_body() filters OpenSearch results to licenses.mostRecentLicenseForType: True.
API Schemas & Response Types
backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/provider/api.py, backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py
Added licenseEligibility field (required, enum: eligible/ineligible) to PublicLicenseSearchResponseSchema and public license search response schema in API model.
Provider-Data Handler Integration
backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/__init__.py, backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/public_lookup.py
get_provider_information() accepts is_public_response flag (default False); public_lookup.py calls with is_public_response=True to return only most-recent licenses per type.
Smoke Test Infrastructure & Coverage
backend/cosmetology-app/tests/smoke/smoke_common.py, backend/cosmetology-app/tests/smoke/public_search_smoke_tests.py, backend/compact-connect/tests/smoke/*.py
Added wait_for_opensearch_sync(), call_public_query_providers(), call_public_get_provider() helpers; updated smoke tests to fetch provider's compact and parameterize configuration; added eligibility mutation tests (expiration, jurisdiction ineligibility) and old-license exclusion test.
Unit & Function Tests
backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py, backend/cosmetology-app/lambdas/python/search/tests/function/*.py, backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/*.py
Updated all test fixtures and assertions to include mostRecentLicenseForType boolean and top-level adverseActions in documents; refactored public search tests with centralized request-body helper and new fixture builders; added tests for eligibility determination across expired, unlifted-adverse, jurisdiction-ineligible, and eligible scenarios.
OpenAPI & Documentation
backend/cosmetology-app/docs/*/*.json, backend/cosmetology-app/docs/postman/*.json, backend/cosmetology-app/tests/resources/snapshots/*.json
Updated OpenAPI specs with licenseEligibility enum field; updated schema component and security authorizer references; regenerated Postman collections; updated response schema snapshot.
Dependencies & Infrastructure
backend/cosmetology-app/*/requirements*.{in,txt}, backend/cosmetology-app/requirements.txt, backend/cosmetology-app/stacks/*.py
Bumped cryptography to >=48, <49; synchronized boto3, botocore, certifi, idna, moto, requests, s3transfer, opensearch-py across modules; removed jinja2 transitive; updated aws-cdk and jsii versions; removed outdated CDK IAM5 suppression; added COG8 suppression for Cognito pool.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • csg-org/CompactConnect#1548: Uses the most-recent license selection logic introduced here to detect provider home jurisdiction changes in ingest/event handling.
  • csg-org/CompactConnect#1239: Prior PR that established staff user MFA recovery docs (updated wording in this PR).

Suggested Reviewers

  • ChiefStief
  • jlkravitz

Poem

🐰 Hops through filters, most recent in sight,
Eligibility checked with adverse delight,
Ancient licenses culled, the young ones shine bright,
Public search now knows which licenses are right!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py (1)

1492-1514: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

licenseEligibility is missing from the public list response contract.

Line 1492+ defines the public search list row schema, but it does not include the new licenseEligibility field in either required or properties. That leaves the API model out of sync with the PR objective and can break frontend/API contract validation.

Proposed schema update
     def _public_license_search_response_schema(self):
         """Schema for public query providers response"""
         stack: AppStack = AppStack.of(self.api)
         return JsonSchema(
             type=JsonSchemaType.OBJECT,
             required=[
                 'providerId',
                 'givenName',
                 'familyName',
                 'licenseJurisdiction',
                 'compact',
                 'licenseType',
                 'licenseNumber',
+                'licenseEligibility',
             ],
             properties={
                 'providerId': JsonSchema(type=JsonSchemaType.STRING, pattern=cc_api.UUID4_FORMAT),
                 'givenName': JsonSchema(type=JsonSchemaType.STRING, min_length=1, max_length=100),
                 'familyName': JsonSchema(type=JsonSchemaType.STRING, min_length=1, max_length=100),
                 'licenseJurisdiction': JsonSchema(
                     type=JsonSchemaType.STRING, enum=stack.node.get_context('jurisdictions')
                 ),
                 'compact': JsonSchema(type=JsonSchemaType.STRING, enum=stack.node.get_context('compacts')),
                 'licenseType': JsonSchema(
                     type=JsonSchemaType.STRING,
                     description='License type or profession designation for this license row',
                 ),
                 'licenseNumber': JsonSchema(type=JsonSchemaType.STRING, min_length=1, max_length=100),
+                'licenseEligibility': JsonSchema(
+                    type=JsonSchemaType.STRING,
+                    enum=['eligible', 'ineligible'],
+                    description='Eligibility status for this license row in public search',
+                ),
             },
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py` around lines
1492 - 1514, The public search list row schema block (the JsonSchema that lists
required=[...]) is missing the new licenseEligibility field; add
"licenseEligibility" to the required list and add a corresponding properties
entry for 'licenseEligibility' (e.g., a JsonSchema with
type=JsonSchemaType.STRING, a short description like "license eligibility status
for this license row", and sensible length/enum constraints consistent with
other fields such as min_length=1 and max_length=100 or an enum if applicable)
so the API model in api_model.py stays in sync with the PR objective.
🧹 Nitpick comments (15)
backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py (1)

469-525: 💤 Low value

Optional: Rename test_find_best_license_* methods to reflect the renamed function.

The test method names (test_find_best_license_date_of_issuance_preferred_when_no_renewal, test_find_best_license_raises_exception_when_no_licenses, test_find_best_license_complex_scenario) still reference the old find_best_license name even though they now call find_most_recently_issued_or_renewed_license. Consider renaming for consistency with the new public API.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py`
around lines 469 - 525, The test method names still reference the old
find_best_license name; update these to match the renamed public API by renaming
test_find_best_license_date_of_issuance_preferred_when_no_renewal,
test_find_best_license_raises_exception_when_no_licenses, and related
test_find_best_license_* methods to use
find_most_recently_issued_or_renewed_license (e.g.,
test_find_most_recently_issued_or_renewed_by_dateOfIssuance_when_no_renewal,
test_find_most_recently_issued_or_renewed_raises_when_no_licenses, etc.) so test
names reflect the function
ProviderRecordUtility.find_most_recently_issued_or_renewed_license and maintain
consistency.
backend/compact-connect/tests/smoke/compact_configuration_smoke_tests.py (1)

106-116: 💤 Low value

Use str | None for the new optional compact parameters.

compact: str = None annotates the parameter as str while allowing None as the default — the rest of this codebase consistently uses str | None = None for optional string parameters. Aligning these signatures keeps type-checkers happy and matches the style used elsewhere.

♻️ Suggested diff
-def test_compact_configuration(compact: str = None):
+def test_compact_configuration(compact: str | None = None):
-def test_jurisdiction_configuration(compact: str = None, jurisdiction: str = 'ne', recreate_compact_config: bool = False):
+def test_jurisdiction_configuration(
+    compact: str | None = None, jurisdiction: str = 'ne', recreate_compact_config: bool = False
+):

Also applies to: 226-237

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/compact-connect/tests/smoke/compact_configuration_smoke_tests.py`
around lines 106 - 116, The parameter annotation for the optional compact
argument in test_compact_configuration should use the union type used elsewhere:
change the signature from compact: str = None to compact: str | None = None;
update any other optional string parameters in the same file (the other test
function(s) around the later occurrence) to follow the same pattern so
type-checkers and code style remain consistent (look for the other function
signatures mentioned in the comment and replace their `str = None` annotations
with `str | None = None`).
backend/cosmetology-app/lambdas/python/search/handlers/public_search.py (1)

47-80: ⚖️ Poor tradeoff

Eligibility helper performs a full provider-schema load per hit — verify performance and intent on validation failure.

A couple of concerns worth confirming:

  1. ProviderOpenSearchDocumentSchema().load(provider_source) is invoked once per OpenSearch hit. For a full page of results this is potentially N full deep validations on data that the indexer already produced. If the per-hit cost is non-trivial, consider whether the eligibility check can be derived directly from the raw _source (e.g., inspecting adverseActions and licenses[0]['compactEligibility'] directly), keeping the schema load as a fallback only when needed.
  2. On ValidationError, the helper currently returns INELIGIBLE. This is a reasonable fail-closed default for the public-facing API, but it does mean any indexer/schema drift will silently surface every affected provider as "ineligible" rather than failing visibly. The logger.error mitigates this somewhat — please confirm that's the intended behavior (vs. e.g. raising and letting it propagate to a 500, since the upstream try/except ValidationError in _public_query_licenses would then just skip the row).
  3. Line 76 unconditionally indexes licenses_list[0], which is safe given the indexing model (one license per provider doc), but _determine_license_eligibility is called with the raw source while _public_query_licenses separately validates source.get('licenses') is non-empty on line 117. Worth a short docstring note that this helper assumes exactly one license entry (matching the indexing model) so future readers don't accidentally call it with multi-license sources.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/cosmetology-app/lambdas/python/search/handlers/public_search.py`
around lines 47 - 80, The helper _determine_license_eligibility currently does a
full ProviderOpenSearchDocumentSchema().load(provider_source) per hit and
returns INELIGIBLE on ValidationError; change it to first attempt a cheap,
direct inspection of provider_source['adverseActions'] and
provider_source.get('licenses', [])[0]['compactEligibility'] to decide
eligibility (only invoking schema.load(...) as a fallback when those raw keys
are missing or malformed), keep the fail-closed behavior but make it explicit by
documenting in the function docstring that ValidationError intentionally maps to
CompactEligibilityStatus.INELIGIBLE and that the function assumes exactly one
license entry (indexing model), and retain the logger.error call so validation
failures remain logged.
backend/cosmetology-app/lambdas/nodejs/lib/email/email-notification-service.ts (1)

67-70: 💤 Low value

Subject line omits the new home state.

The subject is fixed at Practitioner Home State Change - ${compactConfig.compactName} with no provider/state context. The recipient (former state ops team) needs to scan a queue of these — including the practitioner name and/or destination state in the subject (consistent with other notifications in this lambda like "License Encumbrance Notification - John Doe") would substantially improve triage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/nodejs/lib/email/email-notification-service.ts`
around lines 67 - 70, Update the subject construction in
email-notification-service.ts (the subject variable that currently uses
compactConfig.compactName) to include provider identity and the new home state;
for example, build the subject using providerFirstName and providerLastName and
formattedNewJurisdiction (e.g., `Practitioner Home State Change -
${providerFirstName} ${providerLastName} to ${formattedNewJurisdiction} -
${compactConfig.compactName}`) so recipients can triage messages from the inbox
without opening the email.
backend/cosmetology-app/lambdas/nodejs/tests/email-notification-service.test.ts (2)

1082-1094: 💤 Low value

Test fixture has jurisdiction equal to previousJurisdiction, masking a potential bug.

Both jurisdiction and previousJurisdiction are 'tx' in the sample event. Because home_state_change_events.py invokes the lambda with jurisdiction=former_home_jurisdiction (i.e., they intentionally match in production), this fixture is realistic — but it also means the test cannot distinguish the case where the email service incorrectly substitutes one for the other. Consider asserting that the recipient lookup keyed off jurisdiction='tx' matches previousJurisdiction='tx' end-to-end, or alternatively setting previousJurisdiction to a different value so the body assertion on Line 1153 (from TX to OH) catches accidental cross-wiring.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/nodejs/tests/email-notification-service.test.ts`
around lines 1082 - 1094, The fixture
SAMPLE_HOME_JURISDICTION_CHANGE_NEW_STATE_NOTIFICATION_EVENT sets both
jurisdiction and templateVariables.previousJurisdiction to 'tx', which masks
cross-wiring bugs; update the fixture so previousJurisdiction differs (e.g.,
previousJurisdiction: 'tx' and jurisdiction: 'oh' or vice versa) OR add an
explicit end-to-end assertion that the recipient lookup used the
event.jurisdiction (former_home_jurisdiction) rather than
templateVariables.previousJurisdiction—locate
SAMPLE_HOME_JURISDICTION_CHANGE_NEW_STATE_NOTIFICATION_EVENT and modify
templateVariables.previousJurisdiction (or add the recipient lookup assertion
around the email body check that expects "from TX to OH") so the test will fail
if the service substitutes the wrong field.

1160-1170: ⚡ Quick win

Add a missing-jurisdiction test case for parity.

Other State Notification describe blocks in this file (e.g., License Encumbrance State Notification, lines 284–293; Privilege Encumbrance State Notification, lines 564–573) include a test asserting the handler throws when jurisdiction is undefined. The new homeJurisdictionChangeNotification case in lambda.ts (Lines 331–333) has the same guard but no corresponding test.

🧪 Suggested test addition
         it('should throw error when required template variables are missing', async () => {
             const eventWithMissingVariables: EmailNotificationEvent = {
                 ...SAMPLE_HOME_JURISDICTION_CHANGE_NEW_STATE_NOTIFICATION_EVENT,
                 templateVariables: {}
             };

             await expect(lambda.handler(eventWithMissingVariables, {} as any))
                 .rejects
                 .toThrow('Missing required template variables for home jurisdiction change notification template.');
         });
+
+        it('should throw error when jurisdiction is missing', async () => {
+            const eventWithMissingJurisdiction: EmailNotificationEvent = {
+                ...SAMPLE_HOME_JURISDICTION_CHANGE_NEW_STATE_NOTIFICATION_EVENT,
+                jurisdiction: undefined
+            };
+
+            await expect(lambda.handler(eventWithMissingJurisdiction, {} as any))
+                .rejects
+                .toThrow('Missing required jurisdiction field for home jurisdiction change notification template.');
+        });
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/nodejs/tests/email-notification-service.test.ts`
around lines 1160 - 1170, Add a test that mirrors the other "State Notification"
missing-jurisdiction cases: create a copy of
SAMPLE_HOME_JURISDICTION_CHANGE_NEW_STATE_NOTIFICATION_EVENT with jurisdiction
set to undefined (or removed) and assert that calling
lambda.handler(eventWithoutJurisdiction, {} as any) rejects with the same error
thrown by the homeJurisdictionChangeNotification guard. Reference
SAMPLE_HOME_JURISDICTION_CHANGE_NEW_STATE_NOTIFICATION_EVENT and lambda.handler
(and the homeJurisdictionChangeNotification guard behavior in lambda.ts) when
adding the test to the existing describe block so the test suite covers the
missing-jurisdiction path.
backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py (2)

399-407: 💤 Low value

licenseType is sent in the payload but unused by the email lambda.

The Python client populates templateVariables.licenseType (Line 405), but the NodeJS handler case in lambda.ts (Lines 334–340) does not validate or read licenseType, and EmailNotificationService.sendHomeJurisdictionChangeStateNotificationEmail does not accept or render it. This is either dead payload data or a missed inclusion in the email body. Decide whether to drop it from the Python payload or include the license type in the rendered email body/subject (which would also help recipients identify which credential moved when a practitioner holds multiple license types).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py`
around lines 399 - 407, The payload currently includes
templateVariables.licenseType in email_service_client.py but the NodeJS path
doesn't consume it; either remove this dead field from templateVariables in
email_service_client.py, or extend the NodeJS flow to accept and render it:
update the handler that reads the incoming payload (the case that routes to
EmailNotificationService) to extract licenseType, add a parameter to
EmailNotificationService.sendHomeJurisdictionChangeStateNotificationEmail to
accept licenseType, and update the email template rendering logic to include the
license type in subject/body; ensure all call sites and type signatures are
updated accordingly.

41-51: 💤 Low value

Type annotation inconsistent with runtime check.

provider_id: UUID is declared non-Optional on the dataclass (Line 51), but send_provider_home_state_change_email checks if template_variables.provider_id is None (Line 391). The check is unreachable for callers that respect the type, and a static type-checker will flag mypy/pyright errors on the assignment. This matches the pre-existing pattern in InvestigationNotificationTemplateVariables, so it isn't a regression — flagging only for awareness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py`
around lines 41 - 51, The dataclass
HomeJurisdictionChangeNotificationTemplateVariables declares provider_id as UUID
but the runtime code in send_provider_home_state_change_email checks for None;
make the types consistent by changing provider_id: UUID to provider_id:
Optional[UUID] (and add the Optional import) so the None check is valid, or
alternatively remove the None check in send_provider_home_state_change_email;
update the annotation to Optional[UUID] to match the existing pattern used by
InvestigationNotificationTemplateVariables if you want to allow None.
backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py (1)

705-722: ⚡ Quick win

Hardcoded providerId in the asserted event detail is brittle.

Line 712 hardcodes '89a6377e-c3a5-40e5-bca5-317ec854c570' rather than using the provider_id returned from self._with_ingested_license() at line 671. If the fixture (provider-ssn.json) ever changes, the entire Detail JSON string match will fail with a confusing diff. Reuse the existing variable for resilience.

♻️ Suggested fix
                 'Detail': json.dumps(
                     {
                         'compact': 'cosm',
                         'jurisdiction': 'ky',
                         'eventTime': '2024-11-08T23:59:59+00:00',
-                        'providerId': '89a6377e-c3a5-40e5-bca5-317ec854c570',
+                        'providerId': provider_id,
                         'licenseType': 'cosmetologist',
                         'formerHomeJurisdiction': 'oh',
                     }
                 ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py`
around lines 705 - 722, The asserted event detail is brittle because it
hardcodes the provider UUID instead of using the provider_id returned by
self._with_ingested_license(); update the expected object used in the assertion
(the dict compared to home_change_entry) to reference the provider_id variable
(from the self._with_ingested_license() call) for the 'providerId' value and
ensure the json.dumps call uses that provider_id so the test remains resilient
to fixture changes.
backend/cosmetology-app/lambdas/python/common/tests/unit/test_email_service_client.py (1)

49-68: 💤 Low value

Consider comparing parsed JSON instead of exact json.dumps string.

Asserting on Payload=json.dumps({...}) couples the test to a specific dict iteration order in the production code. If the implementation ever changes the key order in its serialization (or switches json.dumps options), this assertion breaks despite the payload being semantically identical. A more resilient approach extracts the Payload kwarg, decodes it, and compares parsed dicts.

♻️ Suggested approach
-        mock_lambda_client.invoke.assert_called_once_with(
-            FunctionName='test-lambda-name',
-            InvocationType='RequestResponse',
-            Payload=json.dumps(
-                {
-                    'compact': TEST_COMPACT,
-                    'jurisdiction': TEST_FORMER_JURISDICTION,
-                    'template': 'homeJurisdictionChangeNotification',
-                    'recipientType': 'JURISDICTION_OPERATIONS_TEAM',
-                    'templateVariables': {
-                        'providerFirstName': 'Jane',
-                        'providerLastName': 'Smith',
-                        'providerId': str(TEST_PROVIDER_ID),
-                        'previousJurisdiction': TEST_FORMER_JURISDICTION,
-                        'newJurisdiction': TEST_NEW_JURISDICTION,
-                        'licenseType': 'Cosmetologist',
-                    },
-                }
-            ),
-        )
+        mock_lambda_client.invoke.assert_called_once()
+        call_kwargs = mock_lambda_client.invoke.call_args.kwargs
+        self.assertEqual('test-lambda-name', call_kwargs['FunctionName'])
+        self.assertEqual('RequestResponse', call_kwargs['InvocationType'])
+        self.assertEqual(
+            {
+                'compact': TEST_COMPACT,
+                'jurisdiction': TEST_FORMER_JURISDICTION,
+                'template': 'homeJurisdictionChangeNotification',
+                'recipientType': 'JURISDICTION_OPERATIONS_TEAM',
+                'templateVariables': {
+                    'providerFirstName': 'Jane',
+                    'providerLastName': 'Smith',
+                    'providerId': str(TEST_PROVIDER_ID),
+                    'previousJurisdiction': TEST_FORMER_JURISDICTION,
+                    'newJurisdiction': TEST_NEW_JURISDICTION,
+                    'licenseType': 'Cosmetologist',
+                },
+            },
+            json.loads(call_kwargs['Payload']),
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/python/common/tests/unit/test_email_service_client.py`
around lines 49 - 68, Update the test to avoid asserting on the exact json.dumps
string: grab the actual call to mock_lambda_client.invoke (e.g. via
mock_lambda_client.invoke.call_args or call_args_list), extract the Payload
kwarg, decode it with json.loads, and compare the resulting dict to the expected
dict (including nested templateVariables) instead of comparing the raw
serialized string; keep the existing assertions for FunctionName and
InvocationType but replace the Payload equality check with a parsed-dict
comparison.
backend/cosmetology-app/tests/smoke/smoke_common.py (2)

438-464: 💤 Low value

Document precedence of jurisdictions over jurisdiction.

When both are provided, jurisdictions silently wins (line 457). That's a reasonable default but isn't reflected in the docstring, which could confuse callers debugging a wrong scope set. Consider noting the precedence in the docstring (or rejecting the case where both are supplied).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/cosmetology-app/tests/smoke/smoke_common.py` around lines 438 - 464,
The docstring for create_test_app_client does not state that the jurisdictions
parameter takes precedence over jurisdiction when both are provided; update the
docstring for create_test_app_client to explicitly document that if both
jurisdictions (list) and jurisdiction (single) are supplied, jurisdictions will
be used to build allowed_scopes (or alternatively implement validation to reject
both), so callers know the precedence of jurisdictions over jurisdiction and how
allowed_scopes is derived.

387-405: 💤 Low value

Edge case: max_attempts can be 0 if poll_interval_seconds > max_wait_time.

With max_wait_time=300 (default) the math works out fine, but if a caller passes a poll_interval_seconds larger than max_wait_time (e.g. max_wait_time=30, poll_interval_seconds=60), max_attempts becomes 0 and the function raises SmokeTestFailureException immediately without ever polling. Either guard with max(1, ...) or document the constraint in the docstring.

♻️ Suggested fix
-    max_attempts = max_wait_time // poll_interval_seconds
+    max_attempts = max(1, max_wait_time // poll_interval_seconds)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/cosmetology-app/tests/smoke/smoke_common.py` around lines 387 - 405,
The code computes max_attempts as "max_wait_time // poll_interval_seconds" which
can yield 0 when poll_interval_seconds > max_wait_time, causing the loop to skip
polling; change the computation of max_attempts to ensure at least one attempt
(e.g., max_attempts = max(1, max_wait_time // poll_interval_seconds)) or
otherwise clamp poll_interval_seconds, so the while loop using attempts <
max_attempts will always perform at least one call to query_provider_by_name
(refer to max_attempts, poll_interval_seconds, max_wait_time and the polling
loop that calls get_staff_user_auth_headers/query_provider_by_name).
backend/cosmetology-app/lambdas/python/data-events/tests/function/test_investigation_events.py (1)

97-98: 💤 Low value

Renamed method still seeds a registered provider — name is now slightly less precise.

The body still seeds compactConnectRegisteredEmailAddress (line 104) so the test specifically exercises the "registered provider" path. Dropping "registered" from the name and docstring makes it ambiguous compared with neighboring tests like test_license_investigation_listener_handles_missing_provider_records. Consider keeping "registered" or adding a brief docstring note clarifying the seeded state. Same applies to lines 161, 222, and 283.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/python/data-events/tests/function/test_investigation_events.py`
around lines 97 - 98, The test method
test_license_investigation_listener_processes_event_with_provider currently
seeds compactConnectRegisteredEmailAddress (a registered provider) but its
name/docs drop "registered", causing ambiguity; either rename the test to
include "registered" (e.g.,
test_license_investigation_listener_processes_event_with_registered_provider) or
update the docstring to explicitly state that the test seeds a registered
provider, and apply the same clarity fix to the other similar tests referenced
around lines 161, 222, and 283 so their names/docs match the seeded state.
backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py (2)

220-227: 💤 Low value

Inconsistent max_wait_seconds between caller and default.

_wait_for_home_state_change_event defaults to 720 seconds (line 112), but the only caller passes 750 (line 222). Either align the default or drop the explicit override to keep the values in sync. Same minor inconsistency exists between wait_for_provider_creation(max_wait_time=750, ...) (line 175) and the helper's default.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py` around
lines 220 - 227, Tests pass an explicit max_wait_seconds=750 to
_wait_for_home_state_change_event and wait_for_provider_creation while the
helpers default to 720; make these consistent by updating the helper defaults to
750 (change the default parameter in _wait_for_home_state_change_event and in
wait_for_provider_creation to 750) so callers can omit the explicit override and
the values remain in sync.

112-137: 💤 Low value

FilterExpression is applied after pagination — may miss events on a busy table.

_wait_for_home_state_change_event runs a single query per attempt and applies FilterExpression='providerId = :provider_id' afterwards. In DynamoDB, the filter is evaluated after the page is read but before results are returned, so if the matching event sits beyond the first page (e.g. many concurrent events on the same pk), it can be silently skipped this attempt. The polling loop will likely catch it on a later attempt, but pagination would be more robust.

In practice this smoke test runs against a sandbox with little contention, so it's a minor reliability concern rather than a bug.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py` around
lines 112 - 137, The query in _wait_for_home_state_change_event uses
FilterExpression='providerId = :provider_id' which is applied client-side per
page and can miss matches on subsequent pages; instead, inside each attempt
iterate through all query pages (use response.get('LastEvaluatedKey') /
ExclusiveStartKey) for the KeyConditionExpression
f'COMPACT#{COMPACT}#JURISDICTION#{HOME_STATE_CHANGE_NEW_JURISDICTION}' and scan
each page's Items for an item whose 'providerId' equals the provider_id
argument; stop and return the matching_event if found, otherwise continue
pagination until exhausted, then sleep and retry the outer attempt loop—keep
ConsistentRead=True and the existing logging around provider.homeStateChange.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/compact-connect/lambdas/python/data-events/requirements-dev.txt`:
- Line 61: The PR pins urllib3==2.7.0 which drops Python 3.9/PyPy3.10 support
and requires pyOpenSSL>=19.0.0; before merging, verify the CI/dev environment
and any consumers support Python >=3.10 (or update target interpreters), add a
note in requirements-dev.txt or project README documenting the Python minimum,
and ensure pyOpenSSL is upgraded (add/raise its version in requirements-dev.txt
or a constraints file) so builds and tests remain green.

In
`@backend/cosmetology-app/lambdas/nodejs/lib/email/email-notification-service.ts`:
- Around line 52-60: The current code assumes getJurisdictionRecipients returns
an array but if jurisdictionConfig.jurisdictionOperationsTeamEmails is missing
it can return undefined causing a TypeError on recipients.length; update the
call-site or the getJurisdictionRecipients implementation so recipients is
always an array (e.g., coerce undefined to [] or return [] from
getJurisdictionRecipients) and then keep the existing check (change the
condition to check for an empty array or falsy/length: recipients = recipients
?? []; if (recipients.length === 0) throw ...). Ensure references:
getJurisdictionRecipients, recipients, and
jurisdictionConfig.jurisdictionOperationsTeamEmails are the places to adjust.

In
`@backend/cosmetology-app/lambdas/python/data-events/handlers/investigation_events.py`:
- Around line 145-146: The handlers in investigation_events.py call
config.data_client.get_provider_top_level_record(...) directly (at the four
sites around lines 146, 213, 274, 341) and thus miss the error-catching/logging
behavior that encumbrance_events.py enforces via its _get_provider_records(...)
wrapper; update investigation_events.py to either call the existing
_get_provider_records(...) helper (or add an equivalent wrapper in
investigation_events.py) so that calls to get_provider_top_level_record are
wrapped to catch exceptions, log failures with processLogger.error (or the
module's logger) and then re-raise, ensuring consistent error visibility for
`@sqs_handler-decorated` handlers.

In
`@backend/cosmetology-app/lambdas/python/data-events/tests/function/test_home_state_change_events.py`:
- Around line 22-23: Update the stale class docstring on
TestHomeStateChangeEvents to accurately describe this test suite (it covers home
state change event handlers, not investigation event handlers); locate the
TestHomeStateChangeEvents class and replace the docstring "Test suite for
investigation event handlers." with a concise description like "Test suite for
home state change event handlers." to reflect its purpose.
- Around line 46-47: Method name has a typo: rename
test_license_homes_state_change_listener_sends_notification_to_former_state to
test_license_home_state_change_listener_sends_notification_to_former_state to
match the docstring and project naming (align with
home_state_change_notification_listener); update any references or test
discovery expectations that use the old name so the test still runs and keep the
docstring unchanged.

In `@backend/cosmetology-app/lambdas/python/search/handlers/public_search.py`:
- Around line 40-44: The tests lack explicit coverage that privilege-level
adverse actions are included in the top-level adverseActions field of OpenSearch
documents; add a new test method in TestGenerateOpenSearchDocuments that uses
get_adverse_action_records() (or the existing test fixtures used by
TestGenerateOpenSearchDocuments) to create at least one privilege-level adverse
action record, call the document generation routine that produces docs (the same
flow used by existing tests like test_license_adverse_actions_included), and
assert that docs[0]['adverseActions'] contains the privilege-level record; also
update or rename any misleading variables like provider_level_adverse_actions in
the test to reflect that adverseActions aggregates provider, license, and
privilege records so the intent is clear.

In `@backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py`:
- Around line 353-354: The error log in the except block for
SmokeTestFailureException still says "License record upload" but the test flow
was renamed; update the logger.error call inside the except
SmokeTestFailureException handler (in license_upload_smoke_tests.py) to use the
current test name by replacing the message string with the new flow name (e.g.,
"license upload smoke test failed" or the exact renamed flow) so the log matches
the test being run while preserving inclusion of str(e).

---

Outside diff comments:
In `@backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py`:
- Around line 1492-1514: The public search list row schema block (the JsonSchema
that lists required=[...]) is missing the new licenseEligibility field; add
"licenseEligibility" to the required list and add a corresponding properties
entry for 'licenseEligibility' (e.g., a JsonSchema with
type=JsonSchemaType.STRING, a short description like "license eligibility status
for this license row", and sensible length/enum constraints consistent with
other fields such as min_length=1 and max_length=100 or an enum if applicable)
so the API model in api_model.py stays in sync with the PR objective.

---

Nitpick comments:
In `@backend/compact-connect/tests/smoke/compact_configuration_smoke_tests.py`:
- Around line 106-116: The parameter annotation for the optional compact
argument in test_compact_configuration should use the union type used elsewhere:
change the signature from compact: str = None to compact: str | None = None;
update any other optional string parameters in the same file (the other test
function(s) around the later occurrence) to follow the same pattern so
type-checkers and code style remain consistent (look for the other function
signatures mentioned in the comment and replace their `str = None` annotations
with `str | None = None`).

In
`@backend/cosmetology-app/lambdas/nodejs/lib/email/email-notification-service.ts`:
- Around line 67-70: Update the subject construction in
email-notification-service.ts (the subject variable that currently uses
compactConfig.compactName) to include provider identity and the new home state;
for example, build the subject using providerFirstName and providerLastName and
formattedNewJurisdiction (e.g., `Practitioner Home State Change -
${providerFirstName} ${providerLastName} to ${formattedNewJurisdiction} -
${compactConfig.compactName}`) so recipients can triage messages from the inbox
without opening the email.

In
`@backend/cosmetology-app/lambdas/nodejs/tests/email-notification-service.test.ts`:
- Around line 1082-1094: The fixture
SAMPLE_HOME_JURISDICTION_CHANGE_NEW_STATE_NOTIFICATION_EVENT sets both
jurisdiction and templateVariables.previousJurisdiction to 'tx', which masks
cross-wiring bugs; update the fixture so previousJurisdiction differs (e.g.,
previousJurisdiction: 'tx' and jurisdiction: 'oh' or vice versa) OR add an
explicit end-to-end assertion that the recipient lookup used the
event.jurisdiction (former_home_jurisdiction) rather than
templateVariables.previousJurisdiction—locate
SAMPLE_HOME_JURISDICTION_CHANGE_NEW_STATE_NOTIFICATION_EVENT and modify
templateVariables.previousJurisdiction (or add the recipient lookup assertion
around the email body check that expects "from TX to OH") so the test will fail
if the service substitutes the wrong field.
- Around line 1160-1170: Add a test that mirrors the other "State Notification"
missing-jurisdiction cases: create a copy of
SAMPLE_HOME_JURISDICTION_CHANGE_NEW_STATE_NOTIFICATION_EVENT with jurisdiction
set to undefined (or removed) and assert that calling
lambda.handler(eventWithoutJurisdiction, {} as any) rejects with the same error
thrown by the homeJurisdictionChangeNotification guard. Reference
SAMPLE_HOME_JURISDICTION_CHANGE_NEW_STATE_NOTIFICATION_EVENT and lambda.handler
(and the homeJurisdictionChangeNotification guard behavior in lambda.ts) when
adding the test to the existing describe block so the test suite covers the
missing-jurisdiction path.

In
`@backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py`:
- Around line 399-407: The payload currently includes
templateVariables.licenseType in email_service_client.py but the NodeJS path
doesn't consume it; either remove this dead field from templateVariables in
email_service_client.py, or extend the NodeJS flow to accept and render it:
update the handler that reads the incoming payload (the case that routes to
EmailNotificationService) to extract licenseType, add a parameter to
EmailNotificationService.sendHomeJurisdictionChangeStateNotificationEmail to
accept licenseType, and update the email template rendering logic to include the
license type in subject/body; ensure all call sites and type signatures are
updated accordingly.
- Around line 41-51: The dataclass
HomeJurisdictionChangeNotificationTemplateVariables declares provider_id as UUID
but the runtime code in send_provider_home_state_change_email checks for None;
make the types consistent by changing provider_id: UUID to provider_id:
Optional[UUID] (and add the Optional import) so the None check is valid, or
alternatively remove the None check in send_provider_home_state_change_email;
update the annotation to Optional[UUID] to match the existing pattern used by
InvestigationNotificationTemplateVariables if you want to allow None.

In
`@backend/cosmetology-app/lambdas/python/common/tests/unit/test_email_service_client.py`:
- Around line 49-68: Update the test to avoid asserting on the exact json.dumps
string: grab the actual call to mock_lambda_client.invoke (e.g. via
mock_lambda_client.invoke.call_args or call_args_list), extract the Payload
kwarg, decode it with json.loads, and compare the resulting dict to the expected
dict (including nested templateVariables) instead of comparing the raw
serialized string; keep the existing assertions for FunctionName and
InvocationType but replace the Payload equality check with a parsed-dict
comparison.

In
`@backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py`:
- Around line 469-525: The test method names still reference the old
find_best_license name; update these to match the renamed public API by renaming
test_find_best_license_date_of_issuance_preferred_when_no_renewal,
test_find_best_license_raises_exception_when_no_licenses, and related
test_find_best_license_* methods to use
find_most_recently_issued_or_renewed_license (e.g.,
test_find_most_recently_issued_or_renewed_by_dateOfIssuance_when_no_renewal,
test_find_most_recently_issued_or_renewed_raises_when_no_licenses, etc.) so test
names reflect the function
ProviderRecordUtility.find_most_recently_issued_or_renewed_license and maintain
consistency.

In
`@backend/cosmetology-app/lambdas/python/data-events/tests/function/test_investigation_events.py`:
- Around line 97-98: The test method
test_license_investigation_listener_processes_event_with_provider currently
seeds compactConnectRegisteredEmailAddress (a registered provider) but its
name/docs drop "registered", causing ambiguity; either rename the test to
include "registered" (e.g.,
test_license_investigation_listener_processes_event_with_registered_provider) or
update the docstring to explicitly state that the test seeds a registered
provider, and apply the same clarity fix to the other similar tests referenced
around lines 161, 222, and 283 so their names/docs match the seeded state.

In
`@backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py`:
- Around line 705-722: The asserted event detail is brittle because it hardcodes
the provider UUID instead of using the provider_id returned by
self._with_ingested_license(); update the expected object used in the assertion
(the dict compared to home_change_entry) to reference the provider_id variable
(from the self._with_ingested_license() call) for the 'providerId' value and
ensure the json.dumps call uses that provider_id so the test remains resilient
to fixture changes.

In `@backend/cosmetology-app/lambdas/python/search/handlers/public_search.py`:
- Around line 47-80: The helper _determine_license_eligibility currently does a
full ProviderOpenSearchDocumentSchema().load(provider_source) per hit and
returns INELIGIBLE on ValidationError; change it to first attempt a cheap,
direct inspection of provider_source['adverseActions'] and
provider_source.get('licenses', [])[0]['compactEligibility'] to decide
eligibility (only invoking schema.load(...) as a fallback when those raw keys
are missing or malformed), keep the fail-closed behavior but make it explicit by
documenting in the function docstring that ValidationError intentionally maps to
CompactEligibilityStatus.INELIGIBLE and that the function assumes exactly one
license entry (indexing model), and retain the logger.error call so validation
failures remain logged.

In `@backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py`:
- Around line 220-227: Tests pass an explicit max_wait_seconds=750 to
_wait_for_home_state_change_event and wait_for_provider_creation while the
helpers default to 720; make these consistent by updating the helper defaults to
750 (change the default parameter in _wait_for_home_state_change_event and in
wait_for_provider_creation to 750) so callers can omit the explicit override and
the values remain in sync.
- Around line 112-137: The query in _wait_for_home_state_change_event uses
FilterExpression='providerId = :provider_id' which is applied client-side per
page and can miss matches on subsequent pages; instead, inside each attempt
iterate through all query pages (use response.get('LastEvaluatedKey') /
ExclusiveStartKey) for the KeyConditionExpression
f'COMPACT#{COMPACT}#JURISDICTION#{HOME_STATE_CHANGE_NEW_JURISDICTION}' and scan
each page's Items for an item whose 'providerId' equals the provider_id
argument; stop and return the matching_event if found, otherwise continue
pagination until exhausted, then sleep and retry the outer attempt loop—keep
ConsistentRead=True and the existing logging around provider.homeStateChange.

In `@backend/cosmetology-app/tests/smoke/smoke_common.py`:
- Around line 438-464: The docstring for create_test_app_client does not state
that the jurisdictions parameter takes precedence over jurisdiction when both
are provided; update the docstring for create_test_app_client to explicitly
document that if both jurisdictions (list) and jurisdiction (single) are
supplied, jurisdictions will be used to build allowed_scopes (or alternatively
implement validation to reject both), so callers know the precedence of
jurisdictions over jurisdiction and how allowed_scopes is derived.
- Around line 387-405: The code computes max_attempts as "max_wait_time //
poll_interval_seconds" which can yield 0 when poll_interval_seconds >
max_wait_time, causing the loop to skip polling; change the computation of
max_attempts to ensure at least one attempt (e.g., max_attempts = max(1,
max_wait_time // poll_interval_seconds)) or otherwise clamp
poll_interval_seconds, so the while loop using attempts < max_attempts will
always perform at least one call to query_provider_by_name (refer to
max_attempts, poll_interval_seconds, max_wait_time and the polling loop that
calls get_staff_user_auth_headers/query_provider_by_name).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c71e096f-0226-409f-9fef-0a499dffaa13

📥 Commits

Reviewing files that changed from the base of the PR and between f5065f5 and 0d07dc2.

⛔ Files ignored due to path filters (2)
  • backend/compact-connect/lambdas/nodejs/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
  • backend/cosmetology-app/lambdas/nodejs/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (75)
  • backend/compact-connect/lambdas/nodejs/package.json
  • backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt
  • backend/compact-connect/lambdas/python/common/requirements-dev.txt
  • backend/compact-connect/lambdas/python/common/requirements.txt
  • backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt
  • backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt
  • backend/compact-connect/lambdas/python/data-events/requirements-dev.txt
  • backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt
  • backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt
  • backend/compact-connect/lambdas/python/feature-flag/requirements.txt
  • backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt
  • backend/compact-connect/lambdas/python/purchases/requirements-dev.in
  • backend/compact-connect/lambdas/python/purchases/requirements-dev.txt
  • backend/compact-connect/lambdas/python/purchases/requirements.in
  • backend/compact-connect/lambdas/python/purchases/requirements.txt
  • backend/compact-connect/lambdas/python/search/requirements-dev.txt
  • backend/compact-connect/lambdas/python/search/requirements.txt
  • backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt
  • backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt
  • backend/compact-connect/requirements-dev.txt
  • backend/compact-connect/tests/smoke/compact_configuration_smoke_tests.py
  • backend/compact-connect/tests/smoke/purchasing_privileges_smoke_tests.py
  • backend/cosmetology-app/lambdas/nodejs/email-notification-service/lambda.ts
  • backend/cosmetology-app/lambdas/nodejs/lib/email/base-email-service.ts
  • backend/cosmetology-app/lambdas/nodejs/lib/email/email-notification-service.ts
  • backend/cosmetology-app/lambdas/nodejs/lib/email/index.ts
  • backend/cosmetology-app/lambdas/nodejs/package.json
  • backend/cosmetology-app/lambdas/nodejs/tests/email-notification-service.test.ts
  • backend/cosmetology-app/lambdas/python/cognito-backup/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/cognito-backup/requirements.txt
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/data_event/api.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/license/api.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/provider/api.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/event_bus_client.py
  • backend/cosmetology-app/lambdas/python/common/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/common/requirements.txt
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_email_service_client.py
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py
  • backend/cosmetology-app/lambdas/python/compact-configuration/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/custom-resources/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/data-events/handlers/encumbrance_events.py
  • backend/cosmetology-app/lambdas/python/data-events/handlers/home_state_change_events.py
  • backend/cosmetology-app/lambdas/python/data-events/handlers/investigation_events.py
  • backend/cosmetology-app/lambdas/python/data-events/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/data-events/tests/function/test_home_state_change_events.py
  • backend/cosmetology-app/lambdas/python/data-events/tests/function/test_investigation_events.py
  • backend/cosmetology-app/lambdas/python/disaster-recovery/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/feature-flag/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/feature-flag/requirements.txt
  • backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/ingest.py
  • backend/cosmetology-app/lambdas/python/provider-data-v1/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py
  • backend/cosmetology-app/lambdas/python/search/handlers/public_search.py
  • backend/cosmetology-app/lambdas/python/search/opensearch_client.py
  • backend/cosmetology-app/lambdas/python/search/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/search/requirements.txt
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_manage_opensearch_indices.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_populate_provider_documents.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_provider_update_ingest.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_public_search_providers.py
  • backend/cosmetology-app/lambdas/python/staff-user-pre-token/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/staff-user-pre-token/tests/test_user_scopes.py
  • backend/cosmetology-app/lambdas/python/staff-user-pre-token/user_data.py
  • backend/cosmetology-app/lambdas/python/staff-users/requirements-dev.txt
  • backend/cosmetology-app/requirements-dev.txt
  • backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py
  • backend/cosmetology-app/stacks/notification_stack.py
  • backend/cosmetology-app/tests/app/test_notification_stack.py
  • backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py
  • backend/cosmetology-app/tests/smoke/smoke_common.py
  • backend/multi-account/README.md
  • backend/multi-account/backups/requirements-dev.txt
  • backend/multi-account/control-tower/requirements-dev.txt
💤 Files with no reviewable changes (1)
  • backend/cosmetology-app/lambdas/nodejs/lib/email/base-email-service.ts

Comment thread backend/cosmetology-app/lambdas/python/search/handlers/public_search.py Outdated
Comment thread backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
backend/cosmetology-app/docs/search-internal/api-specification/latest-oas30.json (1)

343-348: ⚡ Quick win

Consider adding maxItems constraint to error arrays.

The nested error message arrays lack a maxItems constraint, which could allow unbounded arrays in error responses.

Based on learnings, the static analysis tool Checkov flagged this. Consider adding a reasonable limit:

🛡️ Add maxItems constraint
                 "items": {
+                  "maxItems": 100,
                   "type": "array",
                   "description": "List of error messages for a field",
                   "items": {
                     "type": "string"
                   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/docs/search-internal/api-specification/latest-oas30.json`
around lines 343 - 348, The OpenAPI schema has nested error message arrays
without a maxItems constraint; locate the error array schemas near keys like
"licenseType" and "providerId" and add a reasonable "maxItems" (e.g., 50 or 100)
to each array schema that models error lists (commonly named "errors",
"errorMessages", "messages" or similar) so arrays cannot be unbounded; ensure
you add the "maxItems" property alongside the existing "type": "array"
definitions and keep the numeric cap consistent across all error-list schemas.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/cosmetology-app/docs/api-specification/latest-oas30.json`:
- Line 5: The PR modified the wrong OpenAPI file (the State license-management
spec) instead of the Search API; locate the API spec that defines the
/v1/compacts/{compact}/providers/search endpoint and add the new
licenseEligibility field to the provider list response schema (e.g., the
provider compact item schema or the search response schema and its example),
update components/schemas and any examples to include licenseEligibility with
its type and description, and remove/revert the unintended change from the State
API spec so only the search-internal specification contains the new field.

---

Nitpick comments:
In
`@backend/cosmetology-app/docs/search-internal/api-specification/latest-oas30.json`:
- Around line 343-348: The OpenAPI schema has nested error message arrays
without a maxItems constraint; locate the error array schemas near keys like
"licenseType" and "providerId" and add a reasonable "maxItems" (e.g., 50 or 100)
to each array schema that models error lists (commonly named "errors",
"errorMessages", "messages" or similar) so arrays cannot be unbounded; ensure
you add the "maxItems" property alongside the existing "type": "array"
definitions and keep the numeric cap consistent across all error-list schemas.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec6bf3ff-23d8-4f0e-9f7c-7280d299eb10

📥 Commits

Reviewing files that changed from the base of the PR and between 0d07dc2 and c3ff692.

📒 Files selected for processing (12)
  • backend/compact-connect/tests/smoke/compact_configuration_smoke_tests.py
  • backend/cosmetology-app/docs/api-specification/latest-oas30.json
  • backend/cosmetology-app/docs/internal/api-specification/latest-oas30.json
  • backend/cosmetology-app/docs/internal/postman/postman-collection.json
  • backend/cosmetology-app/docs/postman/postman-collection.json
  • backend/cosmetology-app/docs/search-internal/api-specification/latest-oas30.json
  • backend/cosmetology-app/docs/search-internal/postman/postman-collection.json
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py
  • backend/cosmetology-app/lambdas/python/search/handlers/public_search.py
  • backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py
  • backend/cosmetology-app/tests/resources/snapshots/PUBLIC_QUERY_PROVIDERS_RESPONSE_SCHEMA.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py
  • backend/cosmetology-app/lambdas/python/search/handlers/public_search.py
  • backend/compact-connect/tests/smoke/compact_configuration_smoke_tests.py

Comment thread backend/cosmetology-app/docs/api-specification/latest-oas30.json
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
backend/cosmetology-app/tests/smoke/smoke_common.py (1)

530-537: ⚡ Quick win

Normalize jurisdiction scopes to lowercase before client creation.

Scope construction currently uses input casing directly. Normalizing helps avoid mismatched scopes when callers pass uppercase/mixed-case jurisdictions.

Suggested fix
-        jurisdiction_list = jurisdictions if jurisdictions else ([jurisdiction] if jurisdiction else [])
+        jurisdiction_list = jurisdictions if jurisdictions else ([jurisdiction] if jurisdiction else [])
+        jurisdiction_list = [j.lower() for j in jurisdiction_list]
@@
         allowed_scopes = [
             f'{compact}/readGeneral',
             *[f'{jurisdiction}/{compact}.write' for jurisdiction in jurisdiction_list],
         ]

Based on learnings, jurisdiction values in backend/cosmetology-app should follow lowercase normalization for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/cosmetology-app/tests/smoke/smoke_common.py` around lines 530 - 537,
Normalize jurisdiction values to lowercase before building allowed_scopes:
transform jurisdiction_list (created from jurisdictions or jurisdiction) to a
list of lowercased strings (e.g., map .lower() over each element) and then use
that normalized list when constructing allowed_scopes (the list comprehension
f'{jurisdiction}/{compact}.write' and the general read scope). Update references
to jurisdiction_list and allowed_scopes accordingly so scopes are always
generated from lowercase jurisdiction values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/cosmetology-app/tests/smoke/public_search_smoke_tests.py`:
- Around line 132-136: The update_item calls on provider_user_table (e.g., the
call using license_partition_and_sort_key and EXPIRED_DATE_FOR_TEST) are missing
a ConditionExpression and can accidentally upsert if the key is wrong; modify
each provider_user_table.update_item (including the similar calls around the
same area) to include a ConditionExpression that asserts the item exists (for
example attribute_exists on the partition/sort key or a known attribute) and add
any needed ExpressionAttributeNames/Values so DynamoDB will fail instead of
creating a new item when the key is missing or incorrect.

In `@backend/cosmetology-app/tests/smoke/smoke_common.py`:
- Around line 274-276: The current failure handling calls response.json()
unconditionally which will raise if the body is non-JSON; update the POST
handling around the variable response so that when response.status_code != 200
you attempt to parse JSON in a try/except and fall back to response.text() (or a
safe string) and include that fallback body plus response.status_code in the
SmokeTestFailureException message; also avoid calling response.json() twice by
caching the parsed body (e.g., parsed_body) and set page_response_body from that
only when status_code == 200; apply the same pattern to the other occurrence
around lines 295-297.
- Around line 440-443: Ensure poll_interval_seconds is validated and
max_attempts cannot be zero: in the function that accepts max_wait_time,
staff_user_email, poll_interval_seconds validate poll_interval_seconds > 0
(raise or default to a sane positive value) to avoid division by zero, and
compute max_attempts from max_wait_time and poll_interval_seconds using logic
that guarantees at least one attempt (e.g., ceil division or max(1, ...)) so
that when max_wait_time < poll_interval_seconds you still perform one lookup;
update any code that uses the computed max_attempts variable to rely on this
validated/adjusted value.

---

Nitpick comments:
In `@backend/cosmetology-app/tests/smoke/smoke_common.py`:
- Around line 530-537: Normalize jurisdiction values to lowercase before
building allowed_scopes: transform jurisdiction_list (created from jurisdictions
or jurisdiction) to a list of lowercased strings (e.g., map .lower() over each
element) and then use that normalized list when constructing allowed_scopes (the
list comprehension f'{jurisdiction}/{compact}.write' and the general read
scope). Update references to jurisdiction_list and allowed_scopes accordingly so
scopes are always generated from lowercase jurisdiction values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9bcc4f44-11d8-4f64-94cf-873b7102d33b

📥 Commits

Reviewing files that changed from the base of the PR and between c3ff692 and 390f335.

📒 Files selected for processing (2)
  • backend/cosmetology-app/tests/smoke/public_search_smoke_tests.py
  • backend/cosmetology-app/tests/smoke/smoke_common.py

Comment thread backend/cosmetology-app/tests/smoke/public_search_smoke_tests.py
Comment thread backend/cosmetology-app/tests/smoke/smoke_common.py
Comment thread backend/cosmetology-app/tests/smoke/smoke_common.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/license/api.py (1)

57-60: ⚡ Quick win

Add regression assertions for expired-license eligibility correction.

This path now mutates compactEligibility; add explicit unit assertions for compactEligibility == ineligible in API schema expiration tests so this behavior can’t regress silently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/license/api.py`
around lines 57 - 60, The expiration branch now mutates
in_data['compactEligibility'] to CompactEligibilityStatus.INELIGIBLE when
expiration_date < config.expiration_resolution_date; add regression assertions
in the API schema expiration tests to assert that compactEligibility ==
CompactEligibilityStatus.INELIGIBLE (and licenseStatus ==
ActiveInactiveStatus.INACTIVE) for expired-license cases so this behavior cannot
regress silently—update the relevant test(s) that exercise the expiration_date <
config.expiration_resolution_date path to include these explicit assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/license/api.py`:
- Around line 57-60: The expiration branch now mutates
in_data['compactEligibility'] to CompactEligibilityStatus.INELIGIBLE when
expiration_date < config.expiration_resolution_date; add regression assertions
in the API schema expiration tests to assert that compactEligibility ==
CompactEligibilityStatus.INELIGIBLE (and licenseStatus ==
ActiveInactiveStatus.INACTIVE) for expired-license cases so this behavior cannot
regress silently—update the relevant test(s) that exercise the expiration_date <
config.expiration_resolution_date path to include these explicit assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 49bfdd78-da08-4001-93f7-603cbfe4d774

📥 Commits

Reviewing files that changed from the base of the PR and between 390f335 and 8d3d4d3.

📒 Files selected for processing (9)
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/license/api.py
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_provider.py
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py
  • backend/cosmetology-app/lambdas/python/search/opensearch_client.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_manage_opensearch_indices.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_populate_provider_documents.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_provider_update_ingest.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_populate_provider_documents.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py (1)

661-676: ⚡ Quick win

Hoist provider-level adverseActions out of the per-license loop.

self.get_adverse_action_records() and the dict conversion are evaluated on every iteration even though the value is identical for every document (it's a provider-level set). For providers with many licenses and/or many adverse actions this is gratuitous O(L·A) work, and it also diverges from the summary which states provider-level adverse actions are "precomputed and included as a top-level adverseActions array on each OpenSearch document."

♻️ Proposed fix
         provider_dict = self.get_provider_record().to_dict()
         all_privileges = self.generate_privileges_for_provider(include_inactive_privileges=True)

         # Determine the most recent (aka home) license for each license type
         most_recent_licenses = {
             (most_recent_license_for_type.jurisdiction.lower(), most_recent_license_for_type.licenseType)
             for most_recent_license_for_type in self.find_most_recent_licenses_for_each_license_type()
         }

+        provider_adverse_actions = [rec.to_dict() for rec in self.get_adverse_action_records()]
+
         documents = []
         for license_record in self.get_license_records():
@@
             doc = dict(provider_dict)
             doc['licenses'] = [license_dict]
             doc['privileges'] = license_privileges
-            doc['adverseActions'] = [rec.to_dict() for rec in self.get_adverse_action_records()]
+            # Assign a shallow copy so downstream mutations on one doc don't bleed into others.
+            doc['adverseActions'] = list(provider_adverse_actions)
             documents.append(doc)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py`
around lines 661 - 676, The code currently calls
self.get_adverse_action_records() and converts its result to dict inside the
per-license loop, causing redundant O(L·A) work; move this call and the dict
conversion out of the loop so adverse actions are computed once at
provider-level and then reused for each license document. Specifically, in the
function that builds OpenSearch documents (where you iterate over provider
licenses — look for the for ... in provider.get("licenses", []) loop and the use
of self.get_adverse_action_records()), compute adverse_actions =
self.get_adverse_action_records(provider_id or provider_doc) and its dict form
before entering the loop, then remove the per-iteration calls and attach the
precomputed adverse_actions to each produced document as the top-level
adverseActions array.
backend/cosmetology-app/lambdas/python/search/tests/function/test_public_search_providers.py (2)

112-193: 💤 Low value

Consider aligning uploaded status fields with actual status parameters, or document the intentional mismatch.

The helper methods create potentially inconsistent test data:

  • Line 144: jurisdictionUploadedLicenseStatus is always 'active', even when license_status parameter is 'inactive'
  • Line 186: provider-level jurisdictionUploadedCompactEligibility is always 'eligible', even when the nested license may have jurisdiction_uploaded_compact_eligibility='ineligible'

If this mismatch is intentional (to test production correction/normalization logic), consider adding inline comments explaining why the "uploaded" fields differ from the computed fields. Otherwise, align these fields to reduce confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/python/search/tests/function/test_public_search_providers.py`
around lines 112 - 193, The test helper is creating confusingly inconsistent
data: the variable jurisdictionUploadedLicenseStatus is hardcoded to 'active'
regardless of the license_status parameter, and provider-level
jurisdictionUploadedCompactEligibility is hardcoded to 'eligible' regardless of
nested license jurisdiction_uploaded_compact_eligibility; update the helper(s)
that set jurisdictionUploadedLicenseStatus and
jurisdictionUploadedCompactEligibility (search for those exact names or the
helper functions that construct provider/license fixtures) so the "uploaded"
fields are set to the corresponding input parameters (e.g., set
jurisdictionUploadedLicenseStatus = license_status and
jurisdictionUploadedCompactEligibility =
nested_license.jurisdiction_uploaded_compact_eligibility) OR, if the mismatch is
intentional, add concise inline comments at the assignments explaining the
intent and referencing the normalization code being tested.

695-727: ⚡ Quick win

Improve docstring clarity on intentional test data inconsistency.

The test correctly uses inconsistent data—license_status='active' with an expired date_of_expiration='2020-01-01'—to validate that the LicenseExpirationStatusMixin.correct_expired_license_status() @pre_load method properly corrects stale jurisdiction-uploaded statuses.

The docstring "inactive after schema correction" accurately describes this, but could be more explicit. Consider expanding it to: """Verify that the schema corrects a license with an expired date but active status (jurisdiction inconsistency) to ineligible.""" This makes the intentional test design clearer for readers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/python/search/tests/function/test_public_search_providers.py`
around lines 695 - 727, Update the test's docstring to explicitly state that the
test intentionally supplies inconsistent data (license_status='active' with
date_of_expiration='2020-01-01') to verify the
LicenseExpirationStatusMixin.correct_expired_license_status() `@pre_load` logic
converts a jurisdiction-uploaded "active" license with an expired date to
ineligible; for clarity, replace the existing short string with: "Verify that
the schema corrects a license with an expired date but active status
(jurisdiction inconsistency) to ineligible." Reference the test function in
test_public_search_providers.py where the docstring sits and ensure the new
docstring sits immediately above the test that exercises the mixin behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py`:
- Around line 661-676: The code currently calls
self.get_adverse_action_records() and converts its result to dict inside the
per-license loop, causing redundant O(L·A) work; move this call and the dict
conversion out of the loop so adverse actions are computed once at
provider-level and then reused for each license document. Specifically, in the
function that builds OpenSearch documents (where you iterate over provider
licenses — look for the for ... in provider.get("licenses", []) loop and the use
of self.get_adverse_action_records()), compute adverse_actions =
self.get_adverse_action_records(provider_id or provider_doc) and its dict form
before entering the loop, then remove the per-iteration calls and attach the
precomputed adverse_actions to each produced document as the top-level
adverseActions array.

In
`@backend/cosmetology-app/lambdas/python/search/tests/function/test_public_search_providers.py`:
- Around line 112-193: The test helper is creating confusingly inconsistent
data: the variable jurisdictionUploadedLicenseStatus is hardcoded to 'active'
regardless of the license_status parameter, and provider-level
jurisdictionUploadedCompactEligibility is hardcoded to 'eligible' regardless of
nested license jurisdiction_uploaded_compact_eligibility; update the helper(s)
that set jurisdictionUploadedLicenseStatus and
jurisdictionUploadedCompactEligibility (search for those exact names or the
helper functions that construct provider/license fixtures) so the "uploaded"
fields are set to the corresponding input parameters (e.g., set
jurisdictionUploadedLicenseStatus = license_status and
jurisdictionUploadedCompactEligibility =
nested_license.jurisdiction_uploaded_compact_eligibility) OR, if the mismatch is
intentional, add concise inline comments at the assignments explaining the
intent and referencing the normalization code being tested.
- Around line 695-727: Update the test's docstring to explicitly state that the
test intentionally supplies inconsistent data (license_status='active' with
date_of_expiration='2020-01-01') to verify the
LicenseExpirationStatusMixin.correct_expired_license_status() `@pre_load` logic
converts a jurisdiction-uploaded "active" license with an expired date to
ineligible; for clarity, replace the existing short string with: "Verify that
the schema corrects a license with an expired date but active status
(jurisdiction inconsistency) to ineligible." Reference the test function in
test_public_search_providers.py where the docstring sits and ensure the new
docstring sits immediately above the test that exercises the mixin behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7e9ff74c-6cc2-4407-8a57-e5014eaedb2e

📥 Commits

Reviewing files that changed from the base of the PR and between 8d3d4d3 and 5f0ff52.

📒 Files selected for processing (6)
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py
  • backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/__init__.py
  • backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/public_lookup.py
  • backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_public_lookup.py
  • backend/cosmetology-app/lambdas/python/search/handlers/public_search.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_public_search_providers.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/cosmetology-app/lambdas/python/search/handlers/public_search.py

Comment thread backend/cosmetology-app/lambdas/python/search/handlers/public_search.py Outdated
Copy link
Copy Markdown
Collaborator

@ChiefStief ChiefStief left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok looks good, I just had the two questions. One which looks like itll result in some extra filtering based on your response.

@landonshumway-ia
Copy link
Copy Markdown
Collaborator Author

@ChiefStief This is ready for another review, note the LintNode checks are failing due to a detected vulnerability that was just released which doesn't even have a patch yet, so until that is updated we won't be able to address that.

Copy link
Copy Markdown
Collaborator

@ChiefStief ChiefStief left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

@landonshumway-ia
Copy link
Copy Markdown
Collaborator Author

@jlkravitz This is ready for your review. Note that the LintNode checks are failing due to a detected vulnerability that was just released yesterday which doesn't even have a patch yet, so until that is updated we won't be able to address that. Thanks

@landonshumway-ia landonshumway-ia force-pushed the feat/cosm-license-eligibility branch from f9cbffd to 6fcfe3b Compare May 18, 2026 14:22
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py (1)

640-665: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use exact selected records when setting mostRecentLicenseForType.

Line 640 keys “most recent” by (jurisdiction, licenseType). If historical rows exist with the same pair, older rows are also marked mostRecentLicenseForType=True, which breaks the public filtering contract.

💡 Proposed fix
-        most_recent_licenses = {
-            (most_recent_license_for_type.jurisdiction.lower(), most_recent_license_for_type.licenseType)
-            for most_recent_license_for_type in self.find_most_recent_licenses_for_each_license_type()
-        }
+        most_recent_license_by_type = {
+            most_recent_license_for_type.licenseType: most_recent_license_for_type
+            for most_recent_license_for_type in self.find_most_recent_licenses_for_each_license_type()
+        }

@@
-            is_most_recent_license_for_type = (
-                license_record.jurisdiction.lower(),
-                license_record.licenseType,
-            ) in most_recent_licenses
+            is_most_recent_license_for_type = (
+                most_recent_license_by_type.get(license_record.licenseType) is license_record
+            )

Also applies to: 671-671

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py`
around lines 640 - 665, The bug is that most_recent_licenses keys only by
(jurisdiction, licenseType) so older rows with the same pair get marked
mostRecent; change the set built from
find_most_recent_licenses_for_each_license_type() to include a unique identifier
for the selected record (e.g., (jurisdiction.lower(), licenseType,
<unique_id_field>)) and then compute is_most_recent_license_for_type using the
same triple against each license_record from get_license_records(); update
references in the code that set mostRecentLicenseForType to use this stricter
membership test (replace <unique_id_field> with the actual unique property on
license records such as id/licenseId/providerLicenseId).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/cosmetology-app/tests/smoke/public_search_smoke_tests.py`:
- Around line 89-93: get_most_recently_issued_or_renewed_license can return None
but the code immediately calls .get('licenseNumber') which will raise
AttributeError; update the block that calls
get_most_recently_issued_or_renewed_license to check for a falsy/None return
(e.g., if not smoke_license_record) and raise SmokeTestFailureException with a
clear message like "No smoke license record returned for public query" before
attempting to access .get('licenseNumber'), then keep the existing check for a
missing licenseNumber and raise SmokeTestFailureException if it's empty.
- Around line 257-259: The loop over public_provider_detail.get('licenses') can
raise when licenses is missing or null; change it to defensively fetch licenses
(e.g., licenses = public_provider_detail.get('licenses') or []) and iterate
that, or explicitly guard with "if not licenses: licenses = []" before the
for-loop that checks license.get('licenseNumber') against TEST_LICENSE_NUMBER
and raises SmokeTestFailureException; this ensures missing/null licenses won't
cause an exception while preserving the intended test logic.
- Around line 231-239: Validate the result of get_license_type_abbreviation
before using it to build the synthetic SK: after calling
get_license_type_abbreviation(license_type) assign to license_type_abbr and
check if license_type_abbr is None (or falsy); if it is, raise a clear error
(e.g., ValueError or AssertionError) or skip constructing clone['sk'] so you
don't write an invalid key, otherwise build clone['sk'] using COMPACT,
TEST_JURISDICTION and license_type_abbr as before. Ensure the check references
get_license_type_abbreviation, license_type_abbr and clone['sk'] so reviewers
can find the fix easily.

In `@backend/cosmetology-app/tests/smoke/smoke_common.py`:
- Around line 271-287: The loop bounded by _PUBLIC_QUERY_INTERNAL_MAX_PAGES may
stop while page_response_body still contains a last_pagination_key, returning
partial matching_license_rows; change the logic to detect this and fail fast by
raising SmokeTestFailureException when the internal page cap is exhausted but
(page_response_body.get('pagination') or {}).get('lastKey') is still present —
e.g. after the for loop (or immediately when _PUBLIC_QUERY_INTERNAL_MAX_PAGES is
reached) check last_pagination_key and raise a SmokeTestFailureException that
includes the last_pagination_key and the number of pages iterated so far so
callers know the result is incomplete.

---

Outside diff comments:
In
`@backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py`:
- Around line 640-665: The bug is that most_recent_licenses keys only by
(jurisdiction, licenseType) so older rows with the same pair get marked
mostRecent; change the set built from
find_most_recent_licenses_for_each_license_type() to include a unique identifier
for the selected record (e.g., (jurisdiction.lower(), licenseType,
<unique_id_field>)) and then compute is_most_recent_license_for_type using the
same triple against each license_record from get_license_records(); update
references in the code that set mostRecentLicenseForType to use this stricter
membership test (replace <unique_id_field> with the actual unique property on
license records such as id/licenseId/providerLicenseId).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 125cb553-0d09-4a9a-9a92-b951aa879891

📥 Commits

Reviewing files that changed from the base of the PR and between 8d3d4d3 and 6fcfe3b.

📒 Files selected for processing (48)
  • backend/compact-connect/docs/devops/STAFF_USER_MFA_RECOVERY.md
  • backend/compact-connect/tests/smoke/compact_configuration_smoke_tests.py
  • backend/compact-connect/tests/smoke/purchasing_privileges_smoke_tests.py
  • backend/cosmetology-app/compact-config/attestations.yml
  • backend/cosmetology-app/docs/api-specification/latest-oas30.json
  • backend/cosmetology-app/docs/internal/api-specification/latest-oas30.json
  • backend/cosmetology-app/docs/internal/postman/postman-collection.json
  • backend/cosmetology-app/docs/postman/postman-collection.json
  • backend/cosmetology-app/docs/search-internal/api-specification/latest-oas30.json
  • backend/cosmetology-app/docs/search-internal/postman/postman-collection.json
  • backend/cosmetology-app/lambdas/python/cognito-backup/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/cognito-backup/requirements.txt
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/license/api.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/provider/api.py
  • backend/cosmetology-app/lambdas/python/common/requirements-dev.in
  • backend/cosmetology-app/lambdas/python/common/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/common/requirements.in
  • backend/cosmetology-app/lambdas/python/common/requirements.txt
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_provider.py
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py
  • backend/cosmetology-app/lambdas/python/compact-configuration/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/custom-resources/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/data-events/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/disaster-recovery/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/__init__.py
  • backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/public_lookup.py
  • backend/cosmetology-app/lambdas/python/provider-data-v1/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_public_lookup.py
  • backend/cosmetology-app/lambdas/python/search/handlers/public_search.py
  • backend/cosmetology-app/lambdas/python/search/opensearch_client.py
  • backend/cosmetology-app/lambdas/python/search/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/search/requirements.txt
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_manage_opensearch_indices.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_populate_provider_documents.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_provider_update_ingest.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_public_search_providers.py
  • backend/cosmetology-app/lambdas/python/staff-user-pre-token/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/staff-users/requirements-dev.txt
  • backend/cosmetology-app/requirements-dev.txt
  • backend/cosmetology-app/requirements.txt
  • backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py
  • backend/cosmetology-app/stacks/persistent_stack/bulk_uploads_bucket.py
  • backend/cosmetology-app/stacks/state_auth/state_auth_users.py
  • backend/cosmetology-app/tests/resources/snapshots/PUBLIC_QUERY_PROVIDERS_RESPONSE_SCHEMA.json
  • backend/cosmetology-app/tests/smoke/public_search_smoke_tests.py
  • backend/cosmetology-app/tests/smoke/smoke_common.py
💤 Files with no reviewable changes (1)
  • backend/cosmetology-app/compact-config/attestations.yml
✅ Files skipped from review due to trivial changes (6)
  • backend/cosmetology-app/stacks/state_auth/state_auth_users.py
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py
  • backend/compact-connect/docs/devops/STAFF_USER_MFA_RECOVERY.md
  • backend/cosmetology-app/docs/postman/postman-collection.json
  • backend/cosmetology-app/lambdas/python/disaster-recovery/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/provider-data-v1/requirements-dev.txt
🚧 Files skipped from review as they are similar to previous changes (18)
  • backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/public_lookup.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/provider/api.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_manage_opensearch_indices.py
  • backend/cosmetology-app/lambdas/python/search/opensearch_client.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/license/api.py
  • backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/init.py
  • backend/cosmetology-app/docs/search-internal/postman/postman-collection.json
  • backend/cosmetology-app/lambdas/python/search/handlers/public_search.py
  • backend/cosmetology-app/tests/resources/snapshots/PUBLIC_QUERY_PROVIDERS_RESPONSE_SCHEMA.json
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_provider.py
  • backend/compact-connect/tests/smoke/compact_configuration_smoke_tests.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_populate_provider_documents.py
  • backend/cosmetology-app/docs/internal/api-specification/latest-oas30.json
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_public_search_providers.py
  • backend/cosmetology-app/docs/search-internal/api-specification/latest-oas30.json
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py
  • backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_public_lookup.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_provider_update_ingest.py

Comment thread backend/cosmetology-app/tests/smoke/public_search_smoke_tests.py
Comment thread backend/cosmetology-app/tests/smoke/public_search_smoke_tests.py
Comment thread backend/cosmetology-app/tests/smoke/public_search_smoke_tests.py
Comment thread backend/cosmetology-app/tests/smoke/smoke_common.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/compact-connect/tests/smoke/compact_configuration_smoke_tests.py (1)

256-258: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pass through the selected compact when recreating config.

At Line [257], test_compact_configuration() ignores the compact selected for test_jurisdiction_configuration. With recreate_compact_config=True, this can recreate the wrong compact (COMPACTS[0]) and break/misdirect the test flow.

Suggested fix
         if recreate_compact_config:
-            test_compact_configuration()
+            test_compact_configuration(compact=compact)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/compact-connect/tests/smoke/compact_configuration_smoke_tests.py`
around lines 256 - 258, When recreating configs (recreate_compact_config True)
the call to test_compact_configuration() ignores the compact selected earlier
and may recreate the wrong compact; update the call to pass the same compact
chosen by test_jurisdiction_configuration (or the variable holding the selected
compact, e.g., selected_compact or compact) into
test_compact_configuration(compact=...) so the compact recreated matches the
test_jurisdiction_configuration selection (ensure test_compact_configuration
signature accepts a compact parameter or add one if needed).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@backend/cosmetology-app/docs/search-internal/postman/postman-collection.json`:
- Line 468: The sample 200 response for the
/v1/compacts/:compact/providers/search Postman collection is missing the new
licenseEligibility field on provider rows; update the JSON in the "body" by
adding a licenseEligibility property to each provider object inside the
"providers" array (e.g., "licenseEligibility": "eligible" or "ineligible") so
the example matches the current response contract, and regenerate/update the
sample body accordingly.

In `@backend/cosmetology-app/lambdas/python/search/handlers/public_search.py`:
- Around line 39-57: The helper _unlifted_adverse_action_found currently assumes
adverse_actions is an iterable and will raise TypeError if passed None; update
it (or the callers) so None is treated as an empty list: inside
_unlifted_adverse_action_found first check for falsy/None and return False (or
coerce adverse_actions to a list) and ensure
_provider_has_unlifted_adverse_actions_associated_with_license calls
_unlifted_adverse_action_found with license_row.get('adverseActions') and
privilege.get('adverseActions') safely handled; reference functions:
_unlifted_adverse_action_found and
_provider_has_unlifted_adverse_actions_associated_with_license and the keys
license_row.get('adverseActions') and privilege.get('adverseActions').

---

Outside diff comments:
In `@backend/compact-connect/tests/smoke/compact_configuration_smoke_tests.py`:
- Around line 256-258: When recreating configs (recreate_compact_config True)
the call to test_compact_configuration() ignores the compact selected earlier
and may recreate the wrong compact; update the call to pass the same compact
chosen by test_jurisdiction_configuration (or the variable holding the selected
compact, e.g., selected_compact or compact) into
test_compact_configuration(compact=...) so the compact recreated matches the
test_jurisdiction_configuration selection (ensure test_compact_configuration
signature accepts a compact parameter or add one if needed).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 72a2f869-5f4c-4fd6-9562-d1139581467b

📥 Commits

Reviewing files that changed from the base of the PR and between 8d3d4d3 and 6fcfe3b.

📒 Files selected for processing (48)
  • backend/compact-connect/docs/devops/STAFF_USER_MFA_RECOVERY.md
  • backend/compact-connect/tests/smoke/compact_configuration_smoke_tests.py
  • backend/compact-connect/tests/smoke/purchasing_privileges_smoke_tests.py
  • backend/cosmetology-app/compact-config/attestations.yml
  • backend/cosmetology-app/docs/api-specification/latest-oas30.json
  • backend/cosmetology-app/docs/internal/api-specification/latest-oas30.json
  • backend/cosmetology-app/docs/internal/postman/postman-collection.json
  • backend/cosmetology-app/docs/postman/postman-collection.json
  • backend/cosmetology-app/docs/search-internal/api-specification/latest-oas30.json
  • backend/cosmetology-app/docs/search-internal/postman/postman-collection.json
  • backend/cosmetology-app/lambdas/python/cognito-backup/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/cognito-backup/requirements.txt
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/license/api.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/provider/api.py
  • backend/cosmetology-app/lambdas/python/common/requirements-dev.in
  • backend/cosmetology-app/lambdas/python/common/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/common/requirements.in
  • backend/cosmetology-app/lambdas/python/common/requirements.txt
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_provider.py
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_provider_record_util.py
  • backend/cosmetology-app/lambdas/python/compact-configuration/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/custom-resources/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/data-events/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/disaster-recovery/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/__init__.py
  • backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/public_lookup.py
  • backend/cosmetology-app/lambdas/python/provider-data-v1/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_public_lookup.py
  • backend/cosmetology-app/lambdas/python/search/handlers/public_search.py
  • backend/cosmetology-app/lambdas/python/search/opensearch_client.py
  • backend/cosmetology-app/lambdas/python/search/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/search/requirements.txt
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_manage_opensearch_indices.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_populate_provider_documents.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_provider_update_ingest.py
  • backend/cosmetology-app/lambdas/python/search/tests/function/test_public_search_providers.py
  • backend/cosmetology-app/lambdas/python/staff-user-pre-token/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/staff-users/requirements-dev.txt
  • backend/cosmetology-app/requirements-dev.txt
  • backend/cosmetology-app/requirements.txt
  • backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py
  • backend/cosmetology-app/stacks/persistent_stack/bulk_uploads_bucket.py
  • backend/cosmetology-app/stacks/state_auth/state_auth_users.py
  • backend/cosmetology-app/tests/resources/snapshots/PUBLIC_QUERY_PROVIDERS_RESPONSE_SCHEMA.json
  • backend/cosmetology-app/tests/smoke/public_search_smoke_tests.py
  • backend/cosmetology-app/tests/smoke/smoke_common.py
💤 Files with no reviewable changes (1)
  • backend/cosmetology-app/compact-config/attestations.yml
✅ Files skipped from review due to trivial changes (8)
  • backend/cosmetology-app/lambdas/python/common/requirements.in
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py
  • backend/cosmetology-app/stacks/state_auth/state_auth_users.py
  • backend/cosmetology-app/lambdas/python/common/requirements-dev.in
  • backend/compact-connect/docs/devops/STAFF_USER_MFA_RECOVERY.md
  • backend/cosmetology-app/docs/postman/postman-collection.json
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_provider.py
  • backend/cosmetology-app/lambdas/python/disaster-recovery/requirements-dev.txt

{
"_postman_previewlanguage": "json",
"body": "{\n \"providers\": [\n {\n \"birthMonthDay\": \"16-02\",\n \"compact\": \"cosm\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"<date>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"familyName\": \"<string>\",\n \"givenName\": \"<string>\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseJurisdiction\": \"co\",\n \"licenseStatus\": \"active\",\n \"providerId\": \"983077ef-81b6-4e94-a38d-9ecfda81b86a\",\n \"type\": \"provider\",\n \"privileges\": [\n {\n \"administratorSetStatus\": \"inactive\",\n \"compact\": \"cosm\",\n \"dateOfExpiration\": \"2707-10-31\",\n \"jurisdiction\": \"al\",\n \"licenseJurisdiction\": \"md\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"eaa19666-0c94-40e4-a15b-db3ed72c3bb3\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"al\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"c45c1eed-682f-40cf-9586-62f4d524c01c\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"md\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"998f10c3-8571-4c5d-b26c-ba6934198c2b\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"2034-07-13\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1515-10-28\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"va\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"3f479d09-990a-4d9a-b7ba-41df74b00041\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"2271-10-09\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1699-10-22\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2676-02-30\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"5cbf3e2c-da58-4ee0-b7ce-07dec8207bd6\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"2681-04-18\",\n \"liftingUser\": \"<string>\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"cosm\",\n \"dateOfExpiration\": \"2987-06-31\",\n \"jurisdiction\": \"co\",\n \"licenseJurisdiction\": \"oh\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"eae111bd-d3b8-448d-b01d-cc797b006fc0\",\n \"status\": \"inactive\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"va\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"e160d703-de3b-49c7-be24-331f6f3c000b\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"co\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"7fbcb526-c2ec-4d3c-8fd6-3c72870b61d1\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1170-02-31\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2792-01-31\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"9afcaf64-67b0-418d-af53-c8096d813948\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"2220-01-05\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1220-12-21\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2085-06-30\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"3cea388c-d4dc-45a1-9c48-caecd5433902\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"2664-05-20\",\n \"liftingUser\": \"<string>\"\n }\n ]\n }\n ],\n \"suffix\": \"<string>\",\n \"currentHomeJurisdiction\": \"md\",\n \"licenses\": [\n {\n \"compact\": \"cosm\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"1635-05-30\",\n \"dateOfIssuance\": \"2279-12-06\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"familyName\": \"<string>\",\n \"givenName\": \"<string>\",\n \"homeAddressCity\": \"<string>\",\n \"homeAddressPostalCode\": \"<string>\",\n \"homeAddressState\": \"<string>\",\n \"homeAddressStreet1\": \"<string>\",\n \"jurisdiction\": \"va\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseNumber\": \"<string>\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"f5ecbfb0-8245-4dce-bc5c-8e8c3fb720e3\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"<string>\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"ks\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"1caf7375-85f8-40d4-9d88-1d5bc6478576\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"co\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"39eb05fc-2aca-4c31-ba27-24d53ab319f6\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"<string>\",\n \"emailAddress\": \"<email>\",\n \"dateOfRenewal\": \"2768-02-20\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+17859210533\",\n \"licenseStatusName\": \"<string>\",\n \"middleName\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"2973-11-06\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2861-07-13\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"e88c198f-5081-42f4-8f26-fa6e0d3640d0\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"2162-03-10\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"2915-10-31\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2600-09-07\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"5b0a05f2-b595-44d2-9735-1691df5f2068\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"2740-12-31\",\n \"liftingUser\": \"<string>\"\n }\n ]\n },\n {\n \"compact\": \"cosm\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"1447-08-30\",\n \"dateOfIssuance\": \"2140-11-30\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"familyName\": \"<string>\",\n \"givenName\": \"<string>\",\n \"homeAddressCity\": \"<string>\",\n \"homeAddressPostalCode\": \"<string>\",\n \"homeAddressState\": \"<string>\",\n \"homeAddressStreet1\": \"<string>\",\n \"jurisdiction\": \"az\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseNumber\": \"<string>\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"2a06657c-43d8-4e1f-8cf0-d661da262204\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"<string>\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"8de6cacb-65ea-46b8-9349-f540f70e66ab\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"f66dbdd5-ddbe-4e23-846c-c3b36082da8f\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"<string>\",\n \"emailAddress\": \"<email>\",\n \"dateOfRenewal\": \"1091-11-01\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+266910525245\",\n \"licenseStatusName\": \"<string>\",\n \"middleName\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1801-03-31\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1899-03-29\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"ks\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"05c59b30-09dc-484c-a37e-e238db0aa2e1\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"1399-02-08\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1949-02-06\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2961-12-29\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"co\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"d5c72dd2-df42-47e5-9d12-f2a4b0cf4d33\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"2645-06-06\",\n \"liftingUser\": \"<string>\"\n }\n ]\n }\n ],\n \"middleName\": \"<string>\",\n \"compactConnectRegisteredEmailAddress\": \"<email>\"\n },\n {\n \"birthMonthDay\": \"11-31\",\n \"compact\": \"cosm\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"<date>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"familyName\": \"<string>\",\n \"givenName\": \"<string>\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseJurisdiction\": \"al\",\n \"licenseStatus\": \"inactive\",\n \"providerId\": \"3c413612-aff7-422f-9ae3-cac05b1d837f\",\n \"type\": \"provider\",\n \"privileges\": [\n {\n \"administratorSetStatus\": \"inactive\",\n \"compact\": \"cosm\",\n \"dateOfExpiration\": \"2530-12-06\",\n \"jurisdiction\": \"md\",\n \"licenseJurisdiction\": \"ky\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"d9498e8c-9005-4782-b574-6e6cf0e44011\",\n \"status\": \"inactive\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"co\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"76bb1feb-1b15-434e-935d-9be37563997b\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"az\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"064865aa-71b0-4e48-a07a-6c9a8bbdbd76\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1336-06-14\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1196-02-22\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"va\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"06dbbb61-3f81-4f45-b6fc-82443c4f419e\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"1791-11-14\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1113-10-04\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1083-12-02\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"co\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"8d84550a-baec-4842-a5f8-3d1399b7afda\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"2292-10-11\",\n \"liftingUser\": \"<string>\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"cosm\",\n \"dateOfExpiration\": \"1226-12-06\",\n \"jurisdiction\": \"md\",\n \"licenseJurisdiction\": \"az\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"492f0fcd-f0f4-457f-b8da-7a7ee442bcac\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"oh\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"7599e0cf-2c34-4f9b-875c-5ab0ca531c4d\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"e2057bce-3488-4ecc-83df-aec735abc462\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1411-09-30\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1107-02-07\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"088ed538-7672-4dd8-a192-d08bb7129261\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"1999-09-30\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"2451-12-31\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2938-12-01\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"612dae37-a887-4877-9ee1-c7f9da7e17cf\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"1943-03-30\",\n \"liftingUser\": \"<string>\"\n }\n ]\n }\n ],\n \"suffix\": \"<string>\",\n \"currentHomeJurisdiction\": \"al\",\n \"licenses\": [\n {\n \"compact\": \"cosm\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"1630-06-11\",\n \"dateOfIssuance\": \"2424-03-03\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"familyName\": \"<string>\",\n \"givenName\": \"<string>\",\n \"homeAddressCity\": \"<string>\",\n \"homeAddressPostalCode\": \"<string>\",\n \"homeAddressState\": \"<string>\",\n \"homeAddressStreet1\": \"<string>\",\n \"jurisdiction\": \"md\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseNumber\": \"<string>\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"ac33c991-d600-490c-bfca-5f2caac4f117\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"<string>\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"va\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"11cc1dae-f66d-45dd-92a2-fdf7eed8bd86\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"ks\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"cac6a717-28ed-4398-8e70-bdb956bf8ee3\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"<string>\",\n \"emailAddress\": \"<email>\",\n \"dateOfRenewal\": \"2872-03-09\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+2087609290\",\n \"licenseStatusName\": \"<string>\",\n \"middleName\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1858-12-25\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1711-12-28\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"346a41e7-21f9-40f3-aa0a-8e6d8693d732\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"1776-11-30\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1142-03-06\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2399-11-12\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"oh\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"4f9c5550-7026-4660-ad1c-bb825f4fe36d\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"2138-02-26\",\n \"liftingUser\": \"<string>\"\n }\n ]\n },\n {\n \"compact\": \"cosm\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"1870-01-08\",\n \"dateOfIssuance\": \"2155-09-20\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"familyName\": \"<string>\",\n \"givenName\": \"<string>\",\n \"homeAddressCity\": \"<string>\",\n \"homeAddressPostalCode\": \"<string>\",\n \"homeAddressState\": \"<string>\",\n \"homeAddressStreet1\": \"<string>\",\n \"jurisdiction\": \"ks\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseNumber\": \"<string>\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"bd51940e-807e-435d-b9fc-00bd6ad6f7bb\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"<string>\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"az\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"9cb66796-f7f0-4340-9a03-93b2c4b14093\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"oh\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"04350816-776b-4cf7-bfbe-48a5d17f5432\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"<string>\",\n \"emailAddress\": \"<email>\",\n \"dateOfRenewal\": \"1601-11-31\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+9770350103011\",\n \"licenseStatusName\": \"<string>\",\n \"middleName\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"2894-11-30\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1672-10-07\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"f1476062-ec15-46b2-a931-50b4cd7cfbf0\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"1514-10-30\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1782-07-01\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1686-05-30\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"md\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"c59cfdee-1dde-4280-9b69-5f7747e9cfa1\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"<string>\",\n \"<string>\"\n ],\n \"effectiveLiftDate\": \"2431-12-31\",\n \"liftingUser\": \"<string>\"\n }\n ]\n }\n ],\n \"middleName\": \"<string>\",\n \"compactConnectRegisteredEmailAddress\": \"<email>\"\n }\n ],\n \"total\": {\n \"value\": \"<integer>\",\n \"relation\": \"eq\"\n },\n \"lastSort\": \"<array>\"\n}",
"body": "{\n \"providers\": [\n {\n \"birthMonthDay\": \"13-27\",\n \"compact\": \"cosm\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"<date>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"familyName\": \"<string>\",\n \"givenName\": \"<string>\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseJurisdiction\": \"oh\",\n \"licenseStatus\": \"inactive\",\n \"providerId\": \"18bebbfa-e8ca-4869-98c4-ffff83095cc5\",\n \"type\": \"provider\",\n \"privileges\": [\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"cosm\",\n \"dateOfExpiration\": \"1056-09-29\",\n \"jurisdiction\": \"wa\",\n \"licenseJurisdiction\": \"md\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"3d05da9b-c1f5-4592-97f8-3ad8c1146499\",\n \"status\": \"inactive\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"5b6e2c5d-32fb-45d7-9470-0f8b5f305a85\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"db5e051a-9bde-4174-af6a-414a45ee680d\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1994-10-30\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1317-12-01\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"va\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"e22dc06c-faec-4961-835f-3dd6af6fee8b\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"consumer harm\",\n \"other\"\n ],\n \"effectiveLiftDate\": \"1740-02-30\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"2731-11-23\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1810-01-30\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"md\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"b0b0b419-b41d-4927-9587-8f0592b35abf\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"consumer harm\",\n \"consumer harm\"\n ],\n \"effectiveLiftDate\": \"1536-11-16\",\n \"liftingUser\": \"<string>\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"cosm\",\n \"dateOfExpiration\": \"2833-10-15\",\n \"jurisdiction\": \"tn\",\n \"licenseJurisdiction\": \"co\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"4e64bdce-d338-42db-814f-ab46e53e11a0\",\n \"status\": \"inactive\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"md\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"85b4ee0a-e8a0-4e16-b5b7-86ecdd686b38\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"cffb7fe0-9bec-4b44-ac08-56091fce440a\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"2290-01-31\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1055-10-30\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"ks\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"04dcfddc-ff19-40c7-8380-036f041c076a\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"other\",\n \"other\"\n ],\n \"effectiveLiftDate\": \"1323-12-30\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1122-05-09\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2752-08-31\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"az\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"d479b788-5309-4b30-9716-89ada3964fd3\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"consumer harm\",\n \"other\"\n ],\n \"effectiveLiftDate\": \"2600-12-16\",\n \"liftingUser\": \"<string>\"\n }\n ]\n }\n ],\n \"suffix\": \"<string>\",\n \"currentHomeJurisdiction\": \"co\",\n \"licenses\": [\n {\n \"compact\": \"cosm\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"1164-01-21\",\n \"dateOfIssuance\": \"2025-10-04\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"familyName\": \"<string>\",\n \"givenName\": \"<string>\",\n \"homeAddressCity\": \"<string>\",\n \"homeAddressPostalCode\": \"<string>\",\n \"homeAddressState\": \"<string>\",\n \"homeAddressStreet1\": \"<string>\",\n \"jurisdiction\": \"va\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseNumber\": \"<string>\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"ee1923a7-7899-4847-be5d-7b5793f64864\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"<string>\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"b3387c78-bb50-4ba6-9706-dc83e7ba9156\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"ks\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"3c3bc16f-ad2d-4494-aa23-54b5e3c2146f\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"<string>\",\n \"emailAddress\": \"<email>\",\n \"dateOfRenewal\": \"2273-06-31\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+80837827816\",\n \"licenseStatusName\": \"<string>\",\n \"middleName\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1327-12-21\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1923-11-01\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"md\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"1fa7c5ef-ceb2-452c-ba75-f01d08b36332\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"fraud\",\n \"other\"\n ],\n \"effectiveLiftDate\": \"1979-10-31\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1566-03-30\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1493-10-31\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"ks\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"d9793eb4-5bfe-4c79-940b-3955a567f473\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"consumer harm\",\n \"fraud\"\n ],\n \"effectiveLiftDate\": \"2231-10-17\",\n \"liftingUser\": \"<string>\"\n }\n ]\n },\n {\n \"compact\": \"cosm\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"2933-03-12\",\n \"dateOfIssuance\": \"1248-12-31\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"familyName\": \"<string>\",\n \"givenName\": \"<string>\",\n \"homeAddressCity\": \"<string>\",\n \"homeAddressPostalCode\": \"<string>\",\n \"homeAddressState\": \"<string>\",\n \"homeAddressStreet1\": \"<string>\",\n \"jurisdiction\": \"ks\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseNumber\": \"<string>\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"eefd95b6-9870-4dcc-a988-ffa28375aeea\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"<string>\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"6ad74b74-6abd-4fc7-b1cd-0290e10fc256\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"oh\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"bbf87366-cc78-450d-9d95-3079573becb1\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"<string>\",\n \"emailAddress\": \"<email>\",\n \"dateOfRenewal\": \"1790-01-10\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+05715949804\",\n \"licenseStatusName\": \"<string>\",\n \"middleName\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1762-12-16\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2890-11-30\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"al\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"03a1be0c-1e4b-46c4-bb8d-df0ac470eeb0\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"consumer harm\",\n \"consumer harm\"\n ],\n \"effectiveLiftDate\": \"1315-03-31\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1410-09-30\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1082-07-31\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"az\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"e1001d2f-644d-4413-af38-6aefbb9228bf\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"consumer harm\",\n \"other\"\n ],\n \"effectiveLiftDate\": \"2107-10-19\",\n \"liftingUser\": \"<string>\"\n }\n ]\n }\n ],\n \"middleName\": \"<string>\",\n \"compactConnectRegisteredEmailAddress\": \"<email>\"\n },\n {\n \"birthMonthDay\": \"13-15\",\n \"compact\": \"cosm\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"<date>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"familyName\": \"<string>\",\n \"givenName\": \"<string>\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseJurisdiction\": \"al\",\n \"licenseStatus\": \"inactive\",\n \"providerId\": \"02c384d9-c7c1-4bf1-8402-ad53128eb4b7\",\n \"type\": \"provider\",\n \"privileges\": [\n {\n \"administratorSetStatus\": \"inactive\",\n \"compact\": \"cosm\",\n \"dateOfExpiration\": \"1863-12-30\",\n \"jurisdiction\": \"wa\",\n \"licenseJurisdiction\": \"md\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"39e9cb58-baf9-4897-b8bb-cc2956c2696d\",\n \"status\": \"inactive\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"oh\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"3eccb946-d179-4d02-a028-ceb13b435e1c\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"642dd70a-e6a1-462d-ac58-8a668bf05e2c\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"2935-08-17\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2301-11-31\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"ded9577f-5f97-4204-91ab-0cbfed4e2fcc\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"fraud\",\n \"fraud\"\n ],\n \"effectiveLiftDate\": \"2088-06-17\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1620-12-30\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2898-04-20\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"b216230a-42ce-46b0-9ffb-dfc0a3ba10e6\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"fraud\",\n \"other\"\n ],\n \"effectiveLiftDate\": \"1344-12-07\",\n \"liftingUser\": \"<string>\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"cosm\",\n \"dateOfExpiration\": \"1799-11-12\",\n \"jurisdiction\": \"md\",\n \"licenseJurisdiction\": \"wa\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"e6efe045-71ac-4c62-a85c-c383af048d75\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"md\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"8c28f233-25a3-445a-afce-8581dfcb0735\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"07fd36be-4b66-4fdb-88a7-8ad918918b5f\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1818-10-08\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1090-12-08\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"az\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"d83df1f7-0b05-4c21-9f4b-4db4da8f3bec\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"fraud\",\n \"other\"\n ],\n \"effectiveLiftDate\": \"1059-10-30\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"2981-06-31\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1499-09-05\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"va\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"3793e448-9c55-4c58-ba1d-a492b5900424\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"consumer harm\",\n \"other\"\n ],\n \"effectiveLiftDate\": \"1432-03-20\",\n \"liftingUser\": \"<string>\"\n }\n ]\n }\n ],\n \"suffix\": \"<string>\",\n \"currentHomeJurisdiction\": \"wa\",\n \"licenses\": [\n {\n \"compact\": \"cosm\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"1990-01-31\",\n \"dateOfIssuance\": \"2640-06-31\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"familyName\": \"<string>\",\n \"givenName\": \"<string>\",\n \"homeAddressCity\": \"<string>\",\n \"homeAddressPostalCode\": \"<string>\",\n \"homeAddressState\": \"<string>\",\n \"homeAddressStreet1\": \"<string>\",\n \"jurisdiction\": \"tn\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseNumber\": \"<string>\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"e72a1eb4-db31-4daf-84a5-752965792065\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"<string>\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"ks\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"653ae65b-5170-4ef7-b736-0851ff1b5479\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"az\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"798235ed-c128-4919-b444-f7b762408915\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"<string>\",\n \"emailAddress\": \"<email>\",\n \"dateOfRenewal\": \"1967-02-31\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+5188469175\",\n \"licenseStatusName\": \"<string>\",\n \"middleName\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1073-08-30\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2221-05-16\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"fd50f39a-bfaa-4d5f-a864-d443a901b86c\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"fraud\",\n \"other\"\n ],\n \"effectiveLiftDate\": \"2482-12-06\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"2747-12-24\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1309-03-27\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"5650de1a-c7b7-4045-ab5a-b6e9555dd0f0\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"consumer harm\",\n \"other\"\n ],\n \"effectiveLiftDate\": \"1738-03-31\",\n \"liftingUser\": \"<string>\"\n }\n ]\n },\n {\n \"compact\": \"cosm\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"2929-10-11\",\n \"dateOfIssuance\": \"1314-05-07\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"familyName\": \"<string>\",\n \"givenName\": \"<string>\",\n \"homeAddressCity\": \"<string>\",\n \"homeAddressPostalCode\": \"<string>\",\n \"homeAddressState\": \"<string>\",\n \"homeAddressStreet1\": \"<string>\",\n \"jurisdiction\": \"co\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseNumber\": \"<string>\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"beb6d699-b726-4189-9bfd-9c7f37021a65\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"<string>\",\n \"investigations\": [\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"842380bf-3c7d-4310-b691-2552f329bc7b\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"cosm\",\n \"creationDate\": \"<dateTime>\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"investigationId\": \"<string>\",\n \"jurisdiction\": \"md\",\n \"licenseType\": \"<string>\",\n \"providerId\": \"dae71a10-e40d-4a8b-a266-7a01c1f4b542\",\n \"submittingUser\": \"<string>\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"<string>\",\n \"emailAddress\": \"<email>\",\n \"dateOfRenewal\": \"1033-12-05\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+0657366487662\",\n \"licenseStatusName\": \"<string>\",\n \"middleName\": \"<string>\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1480-11-30\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"2751-08-08\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"md\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"2bedc81f-1a98-4753-b168-393787bad1e2\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"consumer harm\",\n \"fraud\"\n ],\n \"effectiveLiftDate\": \"1058-01-01\",\n \"liftingUser\": \"<string>\"\n },\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"<string>\",\n \"compact\": \"cosm\",\n \"creationDate\": \"1999-06-11\",\n \"dateOfUpdate\": \"<dateTime>\",\n \"effectiveStartDate\": \"1422-12-10\",\n \"encumbranceType\": \"<string>\",\n \"jurisdiction\": \"oh\",\n \"licenseType\": \"<string>\",\n \"licenseTypeAbbreviation\": \"<string>\",\n \"providerId\": \"a470dc2b-802b-4d7b-b858-4a286213d4b5\",\n \"submittingUser\": \"<string>\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"consumer harm\",\n \"consumer harm\"\n ],\n \"effectiveLiftDate\": \"1438-12-03\",\n \"liftingUser\": \"<string>\"\n }\n ]\n }\n ],\n \"middleName\": \"<string>\",\n \"compactConnectRegisteredEmailAddress\": \"<email>\"\n }\n ],\n \"total\": {\n \"value\": \"<integer>\",\n \"relation\": \"eq\"\n },\n \"lastSort\": \"<array>\"\n}",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add licenseEligibility to the sample 200 response body.

The example payload for /v1/compacts/:compact/providers/search appears to omit licenseEligibility on provider rows, but this field is now part of the response contract. Please regenerate/update the sample body so it matches the current schema.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/cosmetology-app/docs/search-internal/postman/postman-collection.json`
at line 468, The sample 200 response for the
/v1/compacts/:compact/providers/search Postman collection is missing the new
licenseEligibility field on provider rows; update the JSON in the "body" by
adding a licenseEligibility property to each provider object inside the
"providers" array (e.g., "licenseEligibility": "eligible" or "ineligible") so
the example matches the current response contract, and regenerate/update the
sample body accordingly.

Comment on lines +39 to +57
def _unlifted_adverse_action_found(adverse_actions: list[dict]) -> bool:
for aa in adverse_actions:
if not aa.get('effectiveLiftDate'):
return True
return False


def _provider_has_unlifted_adverse_actions_associated_with_license(
license_row: dict, license_privileges: list[dict]
) -> bool:
# A home state license is determined to be restricted
# if there is an unlifted encumbrance on the license or
# any of the privileges associated with the license
if _unlifted_adverse_action_found(license_row.get('adverseActions')):
return True
for privilege in license_privileges:
if _unlifted_adverse_action_found(privilege.get('adverseActions')):
return True
return False
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle None values for adverseActions to prevent TypeError.

If license_row.get('adverseActions') or privilege.get('adverseActions') returns None, iterating over it in _unlifted_adverse_action_found will raise a TypeError.

Proposed fix
 def _unlifted_adverse_action_found(adverse_actions: list[dict]) -> bool:
+    if not adverse_actions:
+        return False
     for aa in adverse_actions:
         if not aa.get('effectiveLiftDate'):
             return True
     return False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/cosmetology-app/lambdas/python/search/handlers/public_search.py`
around lines 39 - 57, The helper _unlifted_adverse_action_found currently
assumes adverse_actions is an iterable and will raise TypeError if passed None;
update it (or the callers) so None is treated as an empty list: inside
_unlifted_adverse_action_found first check for falsy/None and return False (or
coerce adverse_actions to a list) and ensure
_provider_has_unlifted_adverse_actions_associated_with_license calls
_unlifted_adverse_action_found with license_row.get('adverseActions') and
privilege.get('adverseActions') safely handled; reference functions:
_unlifted_adverse_action_found and
_provider_has_unlifted_adverse_actions_associated_with_license and the keys
license_row.get('adverseActions') and privilege.get('adverseActions').

Copy link
Copy Markdown
Collaborator

@jlkravitz jlkravitz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few small clarifying questions but otherwise looks great!

)
return CompactEligibilityStatus.INELIGIBLE.value

license_row = licenses_list[0]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused by what's going on here. Are we dropping all of the other licenses in the list?

from handlers.public_search import public_search_api_handler

pid = '00000000-0000-0000-0000-0000000000bb'
# create a unlifted adverse action for another license
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm missing something here- this superficially looks like an AA on the license that was searched, so I'd expect it to be ineligible here. But I'm sure I'm missing something in the flow.

f'Public query returned no rows for provider {provider_id} (licenseNumber={license_number})'
)
license_row = matching_license_rows[0]
if license_row.get('licenseEligibility') != 'eligible':
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this being eligible depend on the data in the database when this smoke test is run?

raise SmokeTestFailureException('Smoke license record has no licenseNumber for public query')

logger.info('Running public query endpoint test')
matching_license_rows = call_public_query_providers(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you replace the code block here with a call to _assert_license_eligibility_for_smoke_license?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

in public search, licensees who are not eligible are marked "not eligible" in list view FE

3 participants