Skip to content

Send email notification to former home state when provider home state changes#1548

Open
landonshumway-ia wants to merge 18 commits into
csg-org:mainfrom
InspiringApps:cosm-home-state-change
Open

Send email notification to former home state when provider home state changes#1548
landonshumway-ia wants to merge 18 commits into
csg-org:mainfrom
InspiringApps:cosm-home-state-change

Conversation

@landonshumway-ia
Copy link
Copy Markdown
Collaborator

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

For the Cosmetology compact, the home state for a practitioner is determined by which license was issued or renewed most recently. If another state uploads or renews a license record for that same practitioner with a more recent date, that state becomes the new home state for that practitioner. In that case, the system needs to send a notification to the former home state to let them know that the practitioner's home state has changed.

This adds a new event notification listener that sends an email when triggered. It also updates the license ingest
logic to compare the previous best license with the uploaded license.

Requirements List

  • This change is backwards compatible with the current system.

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 #1543

Summary by CodeRabbit

  • New Features

    • Provider home-jurisdiction change notifications: automated emails to jurisdiction operations teams when a provider's license home state changes.
    • New event listener to process provider home-state-change events.
  • Improvements

    • Ingest flow now detects jurisdiction transitions and emits home-state-change events.
    • Notification handlers use a streamlined provider-record lookup and target appropriate jurisdiction recipients.
  • Tests

    • Added unit, integration, and smoke tests covering event generation, email payloads, handler wiring, and end-to-end flows.
  • Documentation

    • Updated multi-account README policy examples.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Warning

Rate limit exceeded

@landonshumway-ia has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 24 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f98d1e40-b81a-47f2-87fd-9df8e3d12015

📥 Commits

Reviewing files that changed from the base of the PR and between 255916b and d89a671.

⛔ 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 (41)
  • 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/cosmetology-app/lambdas/nodejs/lib/email/email-notification-service.ts
  • backend/cosmetology-app/lambdas/nodejs/package.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/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/common/requirements.txt
  • 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/data-events/tests/function/test_home_state_change_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/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/search/requirements-dev.txt
  • backend/cosmetology-app/lambdas/python/search/requirements.txt
  • 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/multi-account/backups/requirements-dev.txt
  • backend/multi-account/control-tower/requirements-dev.txt
📝 Walkthrough

Walkthrough

This PR implements home jurisdiction change notifications end-to-end: ingest detects same-license-type jurisdiction changes and emits a provider.homeStateChange EventBridge event; NotificationStack delivers events to an SQS listener that calls EmailServiceClient, which invokes the NodeJS email Lambda to resolve former-jurisdiction recipients and send the notification.

Changes

Home Jurisdiction Change Notification

Layer / File(s) Summary
Data Models & Schemas
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/provider_record_util.py
Adds HomeJurisdictionChangeEventDetailSchema and replaces find_best_license with find_most_recently_issued_or_renewed_license (no home-jurisdiction filtering).
Event Generation
backend/cosmetology-app/lambdas/python/common/cc_common/event_bus_client.py
Adds generate_home_jurisdiction_change_event producing schema-validated EventBridge entries (DetailType: provider.homeStateChange).
Ingest Detection
backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/ingest.py
Recomputes best license after persisting uploaded license and queues provider.homeStateChange when the best license's jurisdiction for the licenseType changes.
Python Email Client
backend/cosmetology-app/lambdas/python/common/cc_common/email_service_client.py
Adds HomeJurisdictionChangeNotificationTemplateVariables dataclass and EmailServiceClient.send_provider_home_state_change_email(...) that validates provider_id, maps template variables, and invokes the NodeJS email Lambda.
Event Handling & Provider-record optimization
backend/cosmetology-app/lambdas/python/data-events/handlers/home_state_change_events.py, backend/cosmetology-app/lambdas/python/data-events/handlers/encumbrance_events.py
New SQS listener home_state_change_notification_listener validates event detail, fetches top-level provider record, and calls the email client targeting the former jurisdiction. Notification-only encumbrance handlers now use get_provider_top_level_record.
Investigation Handlers
backend/cosmetology-app/lambdas/python/data-events/handlers/investigation_events.py
Investigation notification listeners now use get_provider_top_level_record and _get_provider_records helper/import removed.
NodeJS Email Service & Rendering
backend/cosmetology-app/lambdas/nodejs/lib/email/email-notification-service.ts, backend/cosmetology-app/lambdas/nodejs/lib/email/base-email-service.ts, backend/cosmetology-app/lambdas/nodejs/lib/email/index.ts
Adds EmailNotificationService with getJurisdictionRecipients and sendHomeJurisdictionChangeStateNotificationEmail; removes tuple/two-column helper methods from BaseEmailService; exports new service from the barrel.
Email Lambda Wiring
backend/cosmetology-app/lambdas/nodejs/email-notification-service/lambda.ts, backend/cosmetology-app/stacks/notification_stack.py
Lambda gains an emailService member, initializes it in the constructor, and adds a homeJurisdictionChangeNotification handler branch; NotificationStack registers a provider home-state-change listener with SQS/EventBridge wiring.
Tests
backend/cosmetology-app/lambdas/nodejs/tests/email-notification-service.test.ts, backend/cosmetology-app/lambdas/python/common/tests/unit/test_email_service_client.py, backend/cosmetology-app/lambdas/python/data-events/tests/function/test_home_state_change_events.py, backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py, backend/cosmetology-app/tests/app/test_notification_stack.py
Adds unit and function tests validating lambda invocation payloads, SES SendEmail parameters and content, EventBridge event emission, handler behavior, and stack resource creation for the new listener.

Staff User Permissions Enhancement

Layer / File(s) Summary
Permission Whitelist Updates
backend/cosmetology-app/lambdas/python/staff-user-pre-token/user_data.py
_process_compact_permissions and _process_jurisdiction_permissions now explicitly include CCPermissionsAction.READ_PRIVATE in allowed-actions validation.
Schema & Test Alignment
backend/cosmetology-app/stacks/api_stack/v1_api/api_model.py, backend/cosmetology-app/lambdas/python/staff-user-pre-token/tests/test_user_scopes.py
API permission schema updated to include readPrivate; test assertion formatting adjusted without behavior change.

Sequence Diagram(s)

sequenceDiagram
    participant Provider as Provider Data<br/>Ingest
    participant EventBus as EventBridge
    participant SQS as SQS Queue
    participant Listener as Home State<br/>Change Listener
    participant Client as Email Service<br/>Client
    participant Lambda as Email<br/>Notification Lambda
    participant SES as SES Service

    Provider->>Provider: Detect home jurisdiction change (best license)
    Provider->>EventBus: Publish provider.homeStateChange event (former jurisdiction)
    EventBus->>SQS: Route event to queue
    Listener->>SQS: Poll message
    Listener->>Listener: Load event detail & extract fields
    Listener->>Client: send_provider_home_state_change_email(former jurisdiction, variables)
    Client->>Lambda: Invoke with homeJurisdictionChangeNotification
    Lambda->>Lambda: Resolve former jurisdiction operations team recipients
    Lambda->>Lambda: Build email template with provider details
    Lambda->>SES: SendEmail to operations team
    SES-->>Lambda: Email sent
    Lambda-->>Client: Success response
    Client-->>Listener: Notification complete
    Listener->>Listener: Log completion
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • jlkravitz
  • ChiefStief
  • jusdino

"🐰 I hopped from schema to stack with nimble feet,
I watched the best-license change and marked the old-state meet,
I fetched just top-level records to keep the run neat,
Then sent the operations team an email, crisp and sweet."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main change: sending email notifications to the former home state when a provider's home state changes.
Description check ✅ Passed The PR description covers requirements, testing checklist, and issue closure, but lacks detailed implementation details of the new listener and schema changes.
Linked Issues check ✅ Passed The PR successfully implements the core requirement from #1543: notifying the former home state when a practitioner's home state changes due to a more recent license being uploaded, without notifying the new home state.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to the home state change notification feature. The refactoring of license selection logic and permission validation are necessary supporting changes for the core feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@landonshumway-ia landonshumway-ia marked this pull request as ready for review May 6, 2026 22:08
@landonshumway-ia landonshumway-ia changed the title Add notification handler for provider home state changes Send email notification to former home state when provider home state changes May 6, 2026
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

🧹 Nitpick comments (3)
backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py (1)

705-722: 💤 Low value

Consider replacing the hardcoded providerId with the captured provider_id variable.

The assertion at line 712 hardcodes '89a6377e-c3a5-40e5-bca5-317ec854c570'. If the SSN fixture changes, this test breaks for an unrelated reason. provider_id is already captured on line 671 and can be used directly.

♻️ Proposed fix
-        self.assertEqual(
-            {
-                'Detail': json.dumps(
-                    {
-                        'compact': 'cosm',
-                        'jurisdiction': 'ky',
-                        'eventTime': '2024-11-08T23:59:59+00:00',
-                        'providerId': '89a6377e-c3a5-40e5-bca5-317ec854c570',
-                        'licenseType': 'cosmetologist',
-                        'formerHomeJurisdiction': 'oh',
-                    }
-                ),
-                'DetailType': 'provider.homeStateChange',
-                'EventBusName': 'license-data-events',
-                'Source': 'org.compactconnect.provider-data',
-            },
-            home_change_entry,
-        )
+        self.assertEqual('provider.homeStateChange', home_change_entry['DetailType'])
+        self.assertEqual('license-data-events', home_change_entry['EventBusName'])
+        self.assertEqual('org.compactconnect.provider-data', home_change_entry['Source'])
+        detail = json.loads(home_change_entry['Detail'])
+        self.assertEqual('cosm', detail['compact'])
+        self.assertEqual('ky', detail['jurisdiction'])
+        self.assertEqual('2024-11-08T23:59:59+00:00', detail['eventTime'])
+        self.assertEqual(provider_id, detail['providerId'])
+        self.assertEqual('cosmetologist', detail['licenseType'])
+        self.assertEqual('oh', detail['formerHomeJurisdiction'])
🤖 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 test assertion hardcodes the provider UUID; replace
the literal '89a6377e-c3a5-40e5-bca5-317ec854c570' with the captured variable
provider_id so the assertion compares home_change_entry to the expected dict
using provider_id (the variable assigned earlier on line ~671) instead of a
fixed string; update the expected payload construction in the assertion that
references 'providerId' to use provider_id to avoid brittle tests.
backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py (1)

93-127: 💤 Low value

LGTM!

The license_type filter is cleanly applied after the existing home_jurisdiction filter, and the two-stage narrowing is correct. The empty-list → CCInternalException path is handled identically to the pre-existing behavior.

One nit: the docstring has two items numbered 2. (lines 104–105). Should be 1/2/3.

🤖 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 93 - 127, Update the docstring in find_best_license to fix the
numbered list (replace the duplicated "2." with the correct sequence 1/2/3) so
the steps read "1. If home jurisdiction..., 2. If license type..., 3. Return the
single license..." — edit the docstring block at the start of the method in
provider_record_util.py (the find_best_license method) to correct the numbering
only.
backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/ingest.py (1)

137-137: ⚡ Quick win

Prefer direct key access ['licenseType'] over .get('licenseType') for fail-fast behavior.

Both find_best_license calls pass license_type=posted_license_record.get('licenseType'). If licenseType were somehow absent from the record, .get() silently returns None, disabling the type filter and causing find_best_license to return the overall best license instead of the best license for the posted type. This could trigger spurious or missed home-state-change notifications across license types.

Since licenseType is required by the ingest schema, using ['licenseType'] would fail fast and make the invariant explicit.

♻️ Proposed fix
-                    current_best_license_for_posted_license_type = ProviderRecordUtility.find_best_license(
-                        license_records=license_records, license_type=posted_license_record.get('licenseType')
-                    )
+                    current_best_license_for_posted_license_type = ProviderRecordUtility.find_best_license(
+                        license_records=license_records, license_type=posted_license_record['licenseType']
+                    )
-            best_license_after_upload_for_posted_license_type = ProviderRecordUtility.find_best_license(
-                license_records=licenses_flattened, license_type=posted_license_record.get('licenseType')
-            )
+            best_license_after_upload_for_posted_license_type = ProviderRecordUtility.find_best_license(
+                license_records=licenses_flattened, license_type=posted_license_record['licenseType']
+            )

Also applies to: 222-224

🤖 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/handlers/ingest.py`
at line 137, The call sites that pass
license_type=posted_license_record.get('licenseType') to find_best_license
should use direct indexing posted_license_record['licenseType'] to fail fast
when the required key is missing; update both occurrences where
find_best_license(..., license_records=license_records,
license_type=posted_license_record.get('licenseType')) appears so the function
receives the explicit value and will raise immediately if 'licenseType' is
absent, preserving the ingest schema invariant and avoiding silent None
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.

Inline comments:
In
`@backend/cosmetology-app/lambdas/nodejs/lib/email/email-notification-service.ts`:
- Around line 18-22: The SPECIFIC branch in getJurisdictionRecipients currently
treats an empty array as present because it checks truthiness; update
getJurisdictionRecipients to explicitly require a non-empty array (e.g., check
Array.isArray(specificEmails) && specificEmails.length > 0) and throw the
existing error if specificEmails is missing or empty; ensure callers like
sendHomeJurisdictionChangeStateNotificationEmail (and any other callers)
continue to rely on the function returning a valid non-empty recipient array or
receiving an exception.

In `@backend/cosmetology-app/stacks/notification_stack.py`:
- Around line 246-258: The construct_id_prefix passed in
_add_provider_home_state_change_notification_listener is not PascalCase; change
the string 'ProviderhomeJurisdictionChangeNotificationListener' to
'ProviderHomeJurisdictionChangeNotificationListener' so CDK logical IDs follow
the established convention; update only the construct_id_prefix argument in the
call inside _add_provider_home_state_change_notification_listener to the
corrected PascalCase value to avoid creating an inconsistent CloudFormation
logical ID.

---

Nitpick comments:
In
`@backend/cosmetology-app/lambdas/python/common/cc_common/data_model/provider_record_util.py`:
- Around line 93-127: Update the docstring in find_best_license to fix the
numbered list (replace the duplicated "2." with the correct sequence 1/2/3) so
the steps read "1. If home jurisdiction..., 2. If license type..., 3. Return the
single license..." — edit the docstring block at the start of the method in
provider_record_util.py (the find_best_license method) to correct the numbering
only.

In `@backend/cosmetology-app/lambdas/python/provider-data-v1/handlers/ingest.py`:
- Line 137: The call sites that pass
license_type=posted_license_record.get('licenseType') to find_best_license
should use direct indexing posted_license_record['licenseType'] to fail fast
when the required key is missing; update both occurrences where
find_best_license(..., license_records=license_records,
license_type=posted_license_record.get('licenseType')) appears so the function
receives the explicit value and will raise immediately if 'licenseType' is
absent, preserving the ingest schema invariant and avoiding silent None
behavior.

In
`@backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py`:
- Around line 705-722: The test assertion hardcodes the provider UUID; replace
the literal '89a6377e-c3a5-40e5-bca5-317ec854c570' with the captured variable
provider_id so the assertion compares home_change_entry to the expected dict
using provider_id (the variable assigned earlier on line ~671) instead of a
fixed string; update the expected payload construction in the assertion that
references 'providerId' to use provider_id to avoid brittle tests.
🪄 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: 357a6f73-4930-45df-b693-ea20a2c35c37

📥 Commits

Reviewing files that changed from the base of the PR and between f5065f5 and 3889a1e.

📒 Files selected for processing (21)
  • 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/tests/email-notification-service.test.ts
  • 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/email_service_client.py
  • backend/cosmetology-app/lambdas/python/common/cc_common/event_bus_client.py
  • backend/cosmetology-app/lambdas/python/common/tests/unit/test_email_service_client.py
  • 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/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/provider-data-v1/handlers/ingest.py
  • backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py
  • 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/stacks/api_stack/v1_api/api_model.py
  • backend/cosmetology-app/stacks/notification_stack.py
💤 Files with no reviewable changes (1)
  • backend/cosmetology-app/lambdas/nodejs/lib/email/base-email-service.ts

Comment thread backend/cosmetology-app/lambdas/nodejs/lib/email/email-notification-service.ts Outdated
Comment thread backend/cosmetology-app/stacks/notification_stack.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: 3

🤖 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/license_upload_smoke_tests.py`:
- Around line 112-138: The query in _wait_for_home_state_change_event only
inspects the first DynamoDB page and can miss matching provider.homeStateChange
events; update the logic around data_events_table.query and
response.get('Items') to handle pagination by looping on
LastEvaluatedKey/ExclusiveStartKey (accumulating or streaming each page) until
the matching item with providerId == provider_id is found or pages exhausted,
preserving ConsistentRead and poll backoff behavior; apply the same pagination
fix to the other helper referenced (the similar query at the lines noted) so
both functions check all pages instead of just the first.
- Around line 141-151: _query_license_ingest_events_for_jurisdiction currently
returns all license.ingest events in a jurisdiction/time window and the finally
block deletes them all; change the query to only return records for the current
test provider before passing them to _cleanup_test_generated_records by adding a
provider parameter (e.g., provider: str) to
_query_license_ingest_events_for_jurisdiction and include the provider in the
KeyConditionExpression or a FilterExpression (e.g., ensure the sk includes
'TYPE#license.ingest#PROVIDER#{provider}#TIME#' or use FilterExpression with
ExpressionAttributeValues[':provider'] and begins_with/contains on sk) so only
that provider's ingest events are returned; update any callers (the finally
cleanup code and places referenced around lines 255-271) to pass the fixture
provider and ensure only those records are forwarded to
_cleanup_test_generated_records.

In `@backend/cosmetology-app/tests/smoke/smoke_common.py`:
- Around line 367-369: The poll loop undercounts attempts because max_attempts =
max_wait_time // poll_interval_seconds omits the initial t=0 attempt and skips
polling when max_wait_time < poll_interval_seconds; update the calculation to
include the initial attempt by using max_attempts = max(1, max_wait_time //
poll_interval_seconds + 1) (using the existing variables max_wait_time and
poll_interval_seconds and assigned to max_attempts) so the loop in the function
that uses max_attempts performs the initial check at t=0 and covers the full
max_wait_time window.
🪄 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: a12aca22-c97e-46e0-bf49-f4f046b73e5f

📥 Commits

Reviewing files that changed from the base of the PR and between e55af90 and 40707a6.

📒 Files selected for processing (4)
  • backend/cosmetology-app/lambdas/nodejs/lib/email/email-notification-service.ts
  • backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py
  • backend/cosmetology-app/tests/smoke/smoke_common.py
  • backend/multi-account/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/cosmetology-app/lambdas/nodejs/lib/email/email-notification-service.ts

Comment thread backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py
Comment thread backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py Outdated
Comment thread backend/cosmetology-app/tests/smoke/smoke_common.py

1. If home jurisdiction is selected, only consider licenses from that jurisdiction
2. If license type is specified, only consider licenses of that type.
2. Return the single license with the latest (renewal date or issuance date)
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.

We've got 1) 2) 2) here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I've cleaned that up.

:param license_type: License type filter
:return: The best license record
"""
# If the provider's home jurisdiction was selected, we only consider licenses from that jurisdiction
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.

This isnt true anymore right?

)
if license_records_in_jurisdiction:
license_records = license_records_in_jurisdiction
license_records = license_records_in_jurisdiction
Copy link
Copy Markdown
Collaborator

@ChiefStief ChiefStief May 8, 2026

Choose a reason for hiding this comment

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

So do they not select a home jurisdiction when registering?

If they do then you could be comparing the new license vs the not-most-recent license because they selected a home jurisdiction in a state that does not have their most recent license.

If they dont and the home state is always based on the same logic then this would work but is a kindof unintuitive and slightly more brittle shortcut.

I guess I would expect the find best license function under these parameters to just look at all their licenses and get the most recent one, not compare the one that caused the flow with the licenses of that type in their home state.

license_records = license_records_in_jurisdiction
license_records = license_records_in_jurisdiction

if license_type is not None:
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.

Separately, Im not 100% understanding the interplay of license types here.

If my most recent license is type A in MA (12/1/2026) but I have type B in MA as well (10/1/2026) and then LA uploads license Type B (11/1/2026), wouldnt we switch home state to LA with this?

Maybe Im just misunderstanding the description of the feature but I thought we would want to stay home state MA in that case?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thank you for walking through the example case. Looking this over you are absolutely right, license type does not play a factor here when determining the practitioner's home state as a whole. The state will simply be associated with the state that issued/renewed a license most recently. I will update the logic accordingly.

self.assertEqual('Audrey', ky_license['givenName'])

self.assertEqual('ky', provider_data['licenseJurisdiction'])
self.assertEqual('Audrey', provider_data['givenName'])
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.

Should we have a negative test too? It uploads but doesnt trigger change?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, I've added this.

self.assertEqual('Audrey', ky_license['givenName'])

self.assertEqual('ky', provider_data['licenseJurisdiction'])
self.assertEqual('Audrey', provider_data['givenName'])
Copy link
Copy Markdown
Collaborator

@ChiefStief ChiefStief May 8, 2026

Choose a reason for hiding this comment

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

Somewhere we should have tests asserting the behavior with MA and LA and different license types that I mentioned.

As it is I dont know what the intended behavior is and so we should codify / concrete that through tests

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 like a good start. I've got some tests I think should get added + some questions about intended functionality. Depending on the answers we may have some bugs.

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/provider_record_util.py (1)

78-90: ⚡ Quick win

Update stale function reference in docstring.

The comment on line 82 references find_best_license, which was renamed to find_most_recently_issued_or_renewed_license in this PR. Update the reference for clarity.

📝 Suggested doc fix
     def _license_sort_key(cls, license_record: dict | LicenseData) -> tuple:
         """
         Sort key for license records: by date of renewal if present, else date of issuance;
-        use date of issuance as tiebreaker. Works with both dict and LicenseData so the same
-        ordering is used in find_best_license (dicts) and find_best_license_in_current_known_licenses (LicenseData).
+        use date of issuance as tiebreaker. Works with both dict and LicenseData so the same ordering
+        is used in find_most_recently_issued_or_renewed_license (dicts) and
+        find_best_license_in_current_known_licenses (LicenseData).
         """
🤖 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 78 - 90, Update the docstring in _license_sort_key to replace the
stale reference to find_best_license with the new function name
find_most_recently_issued_or_renewed_license; ensure the sentence still explains
that the sort key is used by both dict-based and LicenseData-based callers (now
find_most_recently_issued_or_renewed_license and
find_best_license_in_current_known_licenses) so readers can locate where the key
is used.
🤖 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 78-90: Update the docstring in _license_sort_key to replace the
stale reference to find_best_license with the new function name
find_most_recently_issued_or_renewed_license; ensure the sentence still explains
that the sort key is used by both dict-based and LicenseData-based callers (now
find_most_recently_issued_or_renewed_license and
find_best_license_in_current_known_licenses) so readers can locate where the key
is used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bc5c0dbb-c3e2-4c8c-9372-98787b2c944a

📥 Commits

Reviewing files that changed from the base of the PR and between 40707a6 and 255916b.

📒 Files selected for processing (5)
  • 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/provider-data-v1/handlers/ingest.py
  • backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py
  • backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/cosmetology-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py
  • backend/cosmetology-app/tests/smoke/license_upload_smoke_tests.py

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.

Notify former home state when a licensee changes to a new home state

2 participants