Skip to content

Feat/home state change notifications#1279

Merged
isabeleliassen merged 18 commits intocsg-org:mainfrom
InspiringApps:feat/home-state-change-notifications
Feb 2, 2026
Merged

Feat/home state change notifications#1279
isabeleliassen merged 18 commits intocsg-org:mainfrom
InspiringApps:feat/home-state-change-notifications

Conversation

@landonshumway-ia
Copy link
Collaborator

@landonshumway-ia landonshumway-ia commented Jan 27, 2026

When a provider updates their declared home state through the system, the system will now send out notifications to both the previous home state and the new home state.

Requirements List

  • This change is backwards compatible with the current API, as we are just adding a downstream notification process for these events. A feature flag has been added for this that we can disable if needed. Given the low risk nature of this feature, the flag will be enabled by default to all environments it is deployed to.

Description List

  • Added email templates for home jurisdiction change notifications
  • Added logic to publish home state change events onto data event bus
  • Added home state change notification listener for triggering email service lambda to send state notifications
  • Added feature flag for triggering feature on and off
  • Updated smoke tests to verify change in sandbox environment

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
  • Code review
  • Smoke tests

Closes #1258

Summary by CodeRabbit

  • New Features

    • Two new email notifications notify jurisdiction operations teams when a provider’s home state changes; delivery is gated by a runtime feature flag and skipped when the selected jurisdiction is unchanged.
  • Chores

    • Event publishing plus an idempotent listener wired to trigger these notifications; notifications are skipped when no operations emails are configured.
  • Tests

    • Added unit, integration, function, and smoke tests covering content, idempotency, error paths, and end-to-end flows.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

Publishes provider home-jurisdiction-change events, adds idempotent listener that notifies old/new jurisdiction operations teams via new email templates and services, gates publishing with a feature flag, updates data client to return previous jurisdiction/no-op, and adds tests plus infra wiring across Node.js and Python components.

Changes

Cohort / File(s) Summary
Node.js Email Lambda
backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts
Add handlers for two new templates (homeJurisdictionChangeOldStateNotification, homeJurisdictionChangeNewStateNotification) validating templateVariables and delegating to email service.
Node.js Email Service & Tests
backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts, backend/compact-connect/lambdas/nodejs/tests/lib/email/email-notification-service.test.ts, backend/compact-connect/lambdas/nodejs/tests/email-notification-service.test.ts
Add JURISDICTION_OPERATIONS_TEAM recipient type, formatJurisdictionForEmail() helper, and sendHomeJurisdictionChangeStateNotificationEmail(); add tests for rendering, recipient absence, and template validation.
Python data client
backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py
Change update_provider_home_state_jurisdiction() signature to return `str
Python compact-configuration client
backend/compact-connect/lambdas/python/common/cc_common/data_model/compact_configuration_client.py
Add get_jurisdiction_operations_team_emails(compact, jurisdiction) to fetch ops-team emails. Note: duplicate method declaration appears in diff — review duplication.
Python email & event clients
backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py, backend/compact-connect/lambdas/python/common/cc_common/event_bus_client.py
Add methods to invoke email Lambdas for old/new-state notifications and publish_home_jurisdiction_change_event() for the event bus. Note: publish_home_jurisdiction_change_event appears duplicated in the diff.
Python schema & enums
backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/data_event/api.py, backend/compact-connect/lambdas/python/common/cc_common/event_state_client.py, backend/compact-connect/lambdas/python/common/cc_common/feature_flag_enum.py
Add HomeJurisdictionChangeEventDetailSchema, add EventType.HOME_JURISDICTION_CHANGE and LICENSE_INGEST, and add feature flag HOME_JURISDICTION_CHANGE_NOTIFICATION_FLAG.
Python event handler & tests
backend/compact-connect/lambdas/python/data-events/handlers/home_jurisdiction_events.py, backend/compact-connect/lambdas/python/data-events/tests/function/test_home_jurisdiction_events.py
Add idempotent handler home_jurisdiction_change_notification_listener using NotificationTracker; helpers to send old/new notifications; extensive tests covering notifications, idempotency, missing configs, and error paths.
Provider API integration & tests
backend/compact-connect/lambdas/python/provider-data-v1/handlers/provider_users.py, backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_home_jurisdiction_change.py
Capture previous_home_jurisdiction from data client; when feature flag enabled and jurisdiction changes, publish home-jurisdiction-change event (errors logged, non-fatal); add tests for no-op, publish-with-flag, and flag-off behavior.
Stacks / infra wiring
backend/compact-connect/stacks/feature_flag_stack/__init__.py, backend/compact-connect/stacks/notification_stack.py, backend/compact-connect/stacks/api_lambda_stack/__init__.py, backend/compact-connect/stacks/api_lambda_stack/provider_users.py, backend/compact-connect/stacks/persistent_stack/data_event_table.py
Register new feature flag resource, add home-jurisdiction-change notification listener (grant compact-config read access), pass/grant data event bus to provider-users lambdas, and exclude provider.homeJurisdictionChange from general data-event receiver rule.
Tests / Test infra / Smoke
backend/compact-connect/lambdas/python/data-events/tests/function/__init__.py, backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py, backend/compact-connect/tests/resources/snapshots/DATA_EVENT_RULE.json
Add compact-configuration table setup/teardown helpers; add smoke and function tests exercising notification flows, idempotency, error handling, and DB-backed privilege assertions; update EventBridge rule snapshot to exclude provider.homeJurisdictionChange.

Sequence Diagram(s)

sequenceDiagram
    participant ProviderAPI as "Provider API"
    participant Data as "DataClient"
    participant EventBus as "EventBus"
    participant Handler as "HomeJurisdictionHandler"
    participant Config as "CompactConfigClient"
    participant EmailSvc as "EmailServiceClient"
    participant OldOps as "Old Jurisdiction Ops"
    participant NewOps as "New Jurisdiction Ops"

    ProviderAPI->>Data: update_provider_home_state_jurisdiction(compact, provider_id, selected_jurisdiction)
    Data-->>ProviderAPI: previous_home_jurisdiction
    alt previous != new and feature flag enabled
        ProviderAPI->>EventBus: publish_home_jurisdiction_change_event(detail)
        EventBus->>Handler: deliver event
        Handler->>Data: fetch provider record
        Data-->>Handler: provider details
        alt previous is compact and has ops emails
            Handler->>Config: get_jurisdiction_operations_team_emails(previous)
            Config-->>Handler: old_ops_emails
            Handler->>EmailSvc: send_home_jurisdiction_change_old_state_notification(...)
            EmailSvc->>OldOps: email
        end
        alt new is compact and has ops emails
            Handler->>Config: get_jurisdiction_operations_team_emails(new)
            Config-->>Handler: new_ops_emails
            Handler->>EmailSvc: send_home_jurisdiction_change_new_state_notification(...)
            EmailSvc->>NewOps: email
        end
        Handler-->>EventBus: record success (NotificationTracker)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • jlkravitz
  • jusdino
  • isabeleliassen

Poem

🐇 I hopped through code with gentle care,

Told old states and new with emails to share,
Events and flags set my eager motion,
Tests kept my trail neat across the ocean,
Carrots for commits — a happy hare!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.75% 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 title accurately identifies the main feature introduced: notifications when providers change their home state jurisdiction.
Description check ✅ Passed The description covers all required sections with clear requirements, implementation details, and testing steps matching the template.
Linked Issues check ✅ Passed All primary coding objectives from issue #1258 are met: notifications to both old and new states [#1258], feature flag control [#1258], and comprehensive test coverage [#1258].
Out of Scope Changes check ✅ Passed All changes are directly related to implementing home state change notifications. No unrelated modifications or scope creep detected.

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

✨ Finishing touches
  • 📝 Generate docstrings

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 force-pushed the feat/home-state-change-notifications branch 2 times, most recently from 764a760 to bac9c63 Compare January 28, 2026 15:35
@landonshumway-ia landonshumway-ia marked this pull request as ready for review January 28, 2026 17:04
Copy link
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

🤖 Fix all issues with AI agents
In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`:
- Around line 2596-2598: The code currently raises CCInvalidRequestException
when selected_jurisdiction equals
top_level_provider_record.currentHomeJurisdiction
(home_jurisdiction_before_update), making PUT non-idempotent; change this to
treat the request as a no-op: detect equality of selected_jurisdiction and
home_jurisdiction_before_update, do not raise, return the existing
top_level_provider_record/current home jurisdiction immediately, and ensure that
any downstream notification or side-effect logic is executed only when an actual
change occurs (i.e., gate notifications on selected_jurisdiction !=
home_jurisdiction_before_update); update any callers or response path to expect
a no-op return instead of an exception.

In
`@backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py`:
- Around line 848-882: In send_home_jurisdiction_change_new_state_notification,
previous_jurisdiction is typed as str | None but the code calls
previous_jurisdiction.upper() unconditionally; either handle the None case or
enforce non-None. Fix by updating the payload construction in
send_home_jurisdiction_change_new_state_notification so 'previousJurisdiction'
is computed safely (e.g., previous_jurisdiction.upper() if previous_jurisdiction
is not None else '' or a sentinel like 'N/A'), or alternatively change the
function signature/type and add an explicit validation (raise ValueError) if
previous_jurisdiction is None; ensure the change is applied to the
templateVariables entry for previousJurisdiction so no AttributeError can occur.

In
`@backend/compact-connect/lambdas/python/data-events/handlers/home_jurisdiction_events.py`:
- Around line 167-177: Guard against previous_jurisdiction being None before
calling
config.email_service_client.send_home_jurisdiction_change_new_state_notification:
normalize the variable referenced as previous_jurisdiction (used in the call) to
a safe string (e.g. empty string or explicit "UNKNOWN") when it is None so the
email client’s uppercase operation won’t raise; update the block inside the
tracker.should_send_state_notification(new_jurisdiction) branch (where
send_home_jurisdiction_change_new_state_notification is invoked) to replace or
set previous_jurisdiction to a non-None value before passing it.

In
`@backend/compact-connect/lambdas/python/provider-data-v1/handlers/provider_users.py`:
- Around line 111-126: The code currently calls
config.event_bus_client.publish_home_jurisdiction_change_event after
update_provider_home_state_jurisdiction and will surface any publish failure as
a request error; change this to a best‑effort publish by first comparing
previous_home_jurisdiction to selected_jurisdiction and only attempting to
publish if they differ, then wrap the publish call in a try/except that logs the
exception (using the existing logger or config.logger) rather than re-raising so
the API does not fail when event publishing fails; keep the feature flag check
with
is_feature_enabled(FeatureFlagEnum.HOME_JURISDICTION_CHANGE_NOTIFICATION_FLAG,
fail_default=False) intact.

In
`@backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_home_jurisdiction_change.py`:
- Around line 1084-1088: The test docstring/comment in
test_provider_users_home_jurisdiction_change.py is misleading: it mentions
"publishes" and "True" while the test sets mock_is_feature_enabled.return_value
= False; update the docstring/comment above the block (and any inline comment
referencing the flag) to state that the feature flag is off (False) and the
handler provider_users_api_handler should NOT publish the event in this case, so
the comment accurately describes the test behavior.

In `@backend/compact-connect/stacks/feature_flag_stack/__init__.py`:
- Around line 130-142: The comment for
home_jurisdiction_change_notification_flag conflicts with its actual config:
auto_enable_envs on FeatureFlagResource includes
FeatureFlagEnvironmentName.TEST, BETA, and PROD which will auto-enable the flag
with passPercentage=100; either update the comment to state the flag is
auto-enabled in those environments or remove those entries from auto_enable_envs
to require manual enablement—locate the FeatureFlagResource instantiation for
home_jurisdiction_change_notification_flag and either change the comment to
reflect auto-enable behavior or delete the FeatureFlagEnvironmentName.* entries
from auto_enable_envs so the flag truly starts disabled.

In `@backend/compact-connect/stacks/notification_stack.py`:
- Around line 211-214: The emailer_event_listener_handler is granted read access
to persistent_stack.compact_configuration_table but its environment dict is
missing COMPACT_CONFIGURATION_TABLE_NAME; update the handler's environment
(where environment={...} for emailer_event_listener_handler) to include
'COMPACT_CONFIGURATION_TABLE_NAME':
persistent_stack.compact_configuration_table.table_name alongside the existing
PROVIDER_TABLE_NAME, EMAIL_NOTIFICATION_SERVICE_LAMBDA_NAME,
EVENT_STATE_TABLE_NAME and **self.common_env_vars so the handler can initialize
the configuration client.
🧹 Nitpick comments (1)
backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts (1)

563-577: Optional: Simplify the else clause.

The else keyword is unnecessary since the if block returns. This is a minor style nit.

♻️ Suggested simplification
     private formatJurisdictionForEmail(jurisdiction: string): string {
         if (jurisdiction.toLowerCase() === 'other') {
             return 'an unlisted jurisdiction';
         }
-        // else we uppercase the jurisdiction
-        else {
-            return jurisdiction.toUpperCase();
-        }
+        return jurisdiction.toUpperCase();
     }

Copy link
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

🤖 Fix all issues with AI agents
In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`:
- Around line 2596-2605: The code calls .lower() on
top_level_provider_record.currentHomeJurisdiction without guarding for None and
logs a no-op but does not return, so update the no-op handling: in the method
that uses top_level_provider_record.currentHomeJurisdiction and
selected_jurisdiction (the block that logs 'New selected jurisdiction matches
current home state...'), first check for None (e.g., if currentHomeJurisdiction
is None -> skip lower/compare), then perform a case-insensitive comparison only
when both values are non-None; when they match, call logger.info with the
existing context and return early to avoid performing the full update; apply the
same None-guard + early return pattern to the other similar occurrences that use
currentHomeJurisdiction and selected_jurisdiction (the blocks around the other
two occurrences referenced).

In
`@backend/compact-connect/lambdas/python/provider-data-v1/handlers/provider_users.py`:
- Around line 125-148: The current except clause references a non-existent
config.events_client.exceptions.InternalException so publish failures
(ClientError, network, throttling) will propagate; change to catch a broad
exception (e.g., except Exception as e) around the
publish_home_jurisdiction_change_event call in provider_users.py, log the
exception details (include e) along with
compact/previous_home_jurisdiction/selected_jurisdiction/provider_id via
logger.error, and continue execution to preserve best-effort publish semantics
after the DB update.

Copy link
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

🤖 Fix all issues with AI agents
In
`@backend/compact-connect/lambdas/python/provider-data-v1/handlers/provider_users.py`:
- Around line 125-149: The comparison between previous_home_jurisdiction and
selected_jurisdiction can produce false positives because selected_jurisdiction
is lowercased while previous_home_jurisdiction may not be; update the logic in
provider_users.py so you normalize both values (e.g., .lower() or .upper())
before the if check that gates the publish_home_jurisdiction_change_event call,
ensuring you only change the comparison behavior (do not alter the stored value)
and keep the existing exception logging and event payloads unchanged (use the
original variables when publishing the event).

Copy link
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

🤖 Fix all issues with AI agents
In `@backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py`:
- Around line 128-146: The test currently assumes a single privilege by checking
non-empty lists and indexing [0]; update the smoke tests to assert uniqueness by
replacing existence checks with exact-cardinality assertions (e.g., assert
len(ne_privileges) == 1) before using index 0 for all places using
provider_user_records.get_privilege_records (including the blocks around
provider_user_records.get_privilege_records -> ne_privileges and the other
occurrences you flagged at ~294-307 and ~351-364); raise
SmokeTestFailureException with a clear message when len(...) != 1 so duplicates
fail the test, and ensure queries against get_privilege_records are not
paginated when filtering by jurisdiction.
- Around line 26-49: The test_home_jurisdiction_change_to_oh test mutates the
provider user's home jurisdiction and leaves it changed; capture the current
jurisdiction before calling requests.put and restore it in a finally block to
avoid state leaks. Specifically, call the API to read the current home
jurisdiction (using the same auth helper get_provider_user_auth_headers_cached
or existing endpoint) and store it in a variable, perform the PUT to set 'oh' as
now, then in a finally clause issue another PUT to reset the saved original
jurisdiction (and log or raise if the restore fails); reference
test_jurisdiction_configuration, requests.put, and
get_provider_user_auth_headers_cached to locate where to add the capture/restore
logic.

@landonshumway-ia landonshumway-ia force-pushed the feat/home-state-change-notifications branch from 3e0141c to 4153220 Compare January 29, 2026 14:50
@landonshumway-ia landonshumway-ia force-pushed the feat/home-state-change-notifications branch from 4153220 to df276f2 Compare January 29, 2026 17:23
Copy link
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

🤖 Fix all issues with AI agents
In
`@backend/compact-connect/lambdas/python/data-events/tests/function/test_home_jurisdiction_events.py`:
- Around line 137-175: The test sets up previous jurisdiction as
OTHER_JURISDICTION but calls _generate_home_jurisdiction_change_message(None,
'tx'), so update the message payload to use the actual constant
(OTHER_JURISDICTION) for the previous value; change the call to
_generate_home_jurisdiction_change_message(OTHER_JURISDICTION, 'tx') so
home_jurisdiction_change_notification_listener exercises the "previous is other"
branch and the assertions on
send_home_jurisdiction_change_old_state_notification/send_home_jurisdiction_change_new_state_notification
behave as intended.
🧹 Nitpick comments (1)
backend/compact-connect/lambdas/nodejs/tests/email-notification-service.test.ts (1)

1991-2016: Add a happy‑path test for the new‑state notification wiring.

Right now this suite only asserts the missing‑variables error path, so the lambda’s integration (jurisdiction lookup + ops team recipients + SES send) isn’t exercised. A lightweight success test would protect against regressions.

✅ Proposed test addition
     describe('Home Jurisdiction Change New State Notification', () => {
         const SAMPLE_HOME_JURISDICTION_CHANGE_NEW_STATE_NOTIFICATION_EVENT: EmailNotificationEvent = {
             template: 'homeJurisdictionChangeNewStateNotification',
             recipientType: 'JURISDICTION_OPERATIONS_TEAM',
             compact: 'aslp',
             jurisdiction: 'oh',
             templateVariables: {
                 providerFirstName: 'John',
                 providerLastName: 'Doe',
                 providerId: 'provider-123',
                 previousJurisdiction: 'TX',
                 newJurisdiction: 'OH'
             }
         };
 
+        it('should successfully send home jurisdiction change new state notification email', async () => {
+            // Ensure ops-team recipients exist for the jurisdiction
+            mockDynamoDBClient.on(GetItemCommand).callsFake((input) => {
+                if (input.Key.sk.S === 'aslp#JURISDICTION#oh') {
+                    return Promise.resolve({
+                        Item: {
+                            ...SAMPLE_JURISDICTION_CONFIGURATION,
+                            jurisdictionOperationsTeamEmails: { L: [{ S: 'oh-ops@example.com' }] }
+                        }
+                    });
+                }
+                return Promise.resolve({ Item: SAMPLE_COMPACT_CONFIGURATION });
+            });
+
+            const response = await lambda.handler(
+                SAMPLE_HOME_JURISDICTION_CHANGE_NEW_STATE_NOTIFICATION_EVENT,
+                {} as any
+            );
+
+            expect(response).toEqual({ message: 'Email message sent' });
+            expect(mockSESClient).toHaveReceivedCommandWith(SendEmailCommand, {
+                Destination: { ToAddresses: ['oh-ops@example.com'] },
+                Content: expect.any(Object),
+                FromEmailAddress: 'Compact Connect <noreply@example.org>'
+            });
+        });
+
         it('should throw error when required template variables are missing', async () => {
             const eventWithMissingVariables: EmailNotificationEvent = {
                 ...SAMPLE_HOME_JURISDICTION_CHANGE_NEW_STATE_NOTIFICATION_EVENT,
                 templateVariables: {}
             };

Copy link
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

🤖 Fix all issues with AI agents
In
`@backend/compact-connect/lambdas/python/data-events/tests/function/test_home_jurisdiction_events.py`:
- Around line 225-269: The test test_error_handling_records_failure only patches
EmailServiceClient.send_home_jurisdiction_change_old_state_notification, leaving
the new-state send unpatched which can call external code when the handler
continues; update the test to also patch
EmailServiceClient.send_home_jurisdiction_change_new_state_notification (or add
a second `@patch` decorator) and set its return or side_effect appropriately so
the new-state send is stubbed out, ensuring
home_jurisdiction_change_notification_listener runs isolated and only the
old-state failure is exercised.

@landonshumway-ia
Copy link
Collaborator Author

@jlkravitz This is ready for your review. Thanks

Copy link
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 questions!

…ion/test_home_jurisdiction_events.py

Co-authored-by: Joshua Kravitz <jlkravitz@gmail.com>
Copy link
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.

@isabeleliassen Good to merge!

@isabeleliassen isabeleliassen merged commit 6fe0249 into csg-org:main Feb 2, 2026
5 of 6 checks passed
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.

home state change notification

3 participants