Skip to content

[JCC] - Add 'previousSSN' field to license upload process for correcting incorrect SSN uploads#1726

Open
landonshumway-ia wants to merge 21 commits into
csg-org:mainfrom
InspiringApps:feat/jcc-migrate-records
Open

[JCC] - Add 'previousSSN' field to license upload process for correcting incorrect SSN uploads#1726
landonshumway-ia wants to merge 21 commits into
csg-org:mainfrom
InspiringApps:feat/jcc-migrate-records

Conversation

@landonshumway-ia

@landonshumway-ia landonshumway-ia commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

There have been issues where states have entered the wrong SSN for a license record, but the last 4 of the SSN were still accurate so practitioners have been able to register under a license record and purchase privileges. We need to provide a way for states to correct the SSNs for their particular license records. This ticket involves adding a new optional field to the license upload API, 'previousSSN'. When this value is included with a properly formatted SSN, then the system will look up any existing records for the provider id associated with that SSN. If a matching license record is found, it will move that license record along with any associated privileges and other records over to the new provider id, and delete the old records under the previous ssn to remove the duplicate records.

Requirements List

  • This feature is gated with a feature flag so we can disable it if needed to prevent states from using it.

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

Summary by CodeRabbit

  • New Features

    • Added optional previousSSN support to enable SSN corrections that move an existing license and related records to the corrected provider.
    • Introduced an SSN-correction migration feature flag.
    • Added an SSN-correction re-registration email notification for full migrations.
  • Bug Fixes

    • Improved rollback handling when the start/end timestamps fall within the same month but different times of day.
  • Documentation

    • Updated API and ingest documentation to include previousSSN, and expanded smoke-test setup notes.
  • Tests

    • Added unit and regression coverage for SSN-correction routing, migration behavior, and the new notification flow.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds optional previousSSN handling for license uploads, migrates matching provider records and documents between provider IDs, manages Cognito and re-registration notifications, adds feature-flag and infrastructure wiring, and includes migration, rollback, email, and jurisdiction-event tests.

Changes

SSN correction migration

Layer / File(s) Summary
Contracts and migration gating
backend/compact-connect/lambdas/python/common/..., backend/compact-connect/lambdas/python/provider-data-v1/handlers/{bulk_upload,licenses}.py, backend/compact-connect/stacks/...
Adds previousSSN, transient previousProviderId, migration flag handling, and unconditional duplicate SSN validation.
SSN resolution and ingest routing
backend/compact-connect/lambdas/python/provider-data-v1/handlers/ingest.py, .../tests/unit/test_handlers/test_ingest.py
Resolves SSNs to provider IDs, removes raw SSNs from events, and triggers migration before persistence.
Provider record and document migration
backend/compact-connect/lambdas/python/common/cc_common/data_model/..., .../handlers/ingest.py, .../tests/function/...
Moves dependent records transactionally with replay and concurrency handling, and relocates S3 objects during full migrations.
Notification and infrastructure
backend/compact-connect/lambdas/nodejs/..., backend/compact-connect/stacks/..., .../tests/function/test_handlers/test_ingest.py
Adds re-registration email handling, masks email logs, and grants ingest access to Cognito, S3, and notification resources.
End-to-end migration validation
backend/compact-connect/tests/smoke/*
Adds configuration and full/partial migration smoke tests.

Rollback month boundary fix

Layer / File(s) Summary
Rollback query boundary handling
backend/compact-connect/lambdas/python/disaster-recovery/...
Normalizes month-loop timestamps and tests same-month windows with differing times of day.

Home-jurisdiction event behavior

Layer / File(s) Summary
Unconditional jurisdiction event publication
backend/compact-connect/lambdas/python/provider-data-v1/handlers/provider_users.py, .../test_provider_users_home_jurisdiction_change.py
Publishes jurisdiction-change events whenever the selected jurisdiction differs from the previous value.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StateAPI
  participant IngestPreprocessor
  participant DataClient
  participant S3
  participant Cognito
  participant EmailService
  StateAPI->>IngestPreprocessor: submit corrected license with previousSSN
  IngestPreprocessor->>DataClient: resolve provider IDs
  IngestPreprocessor->>DataClient: migrate provider records
  DataClient->>S3: move provider objects
  DataClient->>Cognito: delete old provider user
  Cognito->>EmailService: request re-registration email
Loading

Possibly related PRs

Suggested reviewers: jlkravitz, jusdino, isabeleliassen

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The home-jurisdiction notification flag removal in provider_users.py is unrelated to SSN correction migration work. Move the provider home-jurisdiction notification flag change and its test updates into a separate PR.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the new previousSSN upload support and SSN correction use case.
Description check ✅ Passed The description covers the main change and testing, though it omits the template's Description List section.
Linked Issues check ✅ Passed The PR adds previousSSN-driven migration, related record moves, and feature-flag gating as requested in #1725.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

🤖 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/nodejs/lib/email/email-notification-service.ts`:
- Line 440: Avoid logging recipient email addresses in the ssn correction
reregistration notification flow. Update the logger call in the
email-notification service method that sends this notification so it keeps only
safe metadata about the compact/practitioner and removes the
recipients/specificEmails payload. If you need traceability, log a non-PII
indicator such as recipient count or delivery type instead, and apply the same
sanitizing pattern used by other notification methods in this file.

In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`:
- Around line 3154-3175: The new-provider record creation path in
_build_new_provider_record_if_absent is race-prone because the existence check
and the later Put are not atomic. Update the write path used for the provider
summary record so the Put includes a condition like attribute_not_exists(pk),
matching the other conditional writes in this flow, and keep the existing
get_provider_top_level_record / ProviderRecordUtility.populate_provider_record
logic for the pre-check and record construction.
- Around line 2931-2932: `full_teardown` is being decided from a stale provider
snapshot, so add a commit-time concurrency guard in the teardown path. Update
`update_provider_license_and_schema_data` to revalidate the old provider’s
license set against the current database state immediately before deleting the
provider record, not just via the existing `dateOfUpdate` fence. Use the same
teardown/delete flow that currently handles `old_provider_records`,
`old_provider_license_records`, and the provider deletion block to ensure a
concurrent ingest cannot add a license and leave it orphaned.
- Around line 2887-2891: The SSN-correction migration currently infers
teardown/retry state from whether `old_provider_data` still exists, which breaks
replay after a later DynamoDB batch failure past the 100-item limit. Update the
migration flow in `data_client.py` to persist an explicit cleanup/teardown
marker (or equivalent state) when the old provider record is deleted, and have
the replay path use that marker instead of `old_provider_data is None`. Adjust
the logic around the `old_provider_registered_email` / Cognito delete /
re-registration email branch so it still runs correctly on retry even when the
old provider record has already been removed.

In `@backend/compact-connect/lambdas/python/provider-data-v1/handlers/ingest.py`:
- Around line 373-478: The teardown work in _perform_ssn_correction_migration is
coupled too tightly to migrate_provider_for_ssn_correction, so once
migration_performed becomes false on a retry,
_move_provider_documents_to_new_keyspace and
_delete_old_cognito_user_and_send_reregistration_email never run. Add a separate
persisted teardown state (for example in the existing migration result or a new
tracking record) so the S3 move and Cognito/email steps can resume independently
after a crash. Update the retry path in _perform_ssn_correction_migration to
check that teardown state rather than relying only on migration_performed, and
keep _move_s3_object and _delete_old_cognito_user_and_send_reregistration_email
idempotent.
🪄 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: e728dc78-699b-468b-8b55-c9d6593ab8fb

📥 Commits

Reviewing files that changed from the base of the PR and between a8102ac and 615fbc8.

📒 Files selected for processing (27)
  • backend/compact-connect/docs/README.md
  • backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts
  • backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts
  • backend/compact-connect/lambdas/nodejs/tests/email-notification-service.test.ts
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/provider_record_util.py
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/common.py
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/api.py
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/license/ingest.py
  • backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py
  • backend/compact-connect/lambdas/python/common/cc_common/feature_flag_enum.py
  • backend/compact-connect/lambdas/python/common/cc_common/utils.py
  • backend/compact-connect/lambdas/python/common/tests/unit/test_data_model/test_schema/test_license.py
  • backend/compact-connect/lambdas/python/common/tests/unit/test_email_service_client.py
  • backend/compact-connect/lambdas/python/disaster-recovery/handlers/rollback_license_upload.py
  • backend/compact-connect/lambdas/python/disaster-recovery/tests/function/test_rollback_license_upload.py
  • backend/compact-connect/lambdas/python/provider-data-v1/handlers/bulk_upload.py
  • backend/compact-connect/lambdas/python/provider-data-v1/handlers/ingest.py
  • backend/compact-connect/lambdas/python/provider-data-v1/handlers/licenses.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_bulk_upload.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_licenses.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/unit/test_handlers/test_ingest.py
  • backend/compact-connect/pipeline/backend_stage.py
  • backend/compact-connect/stacks/feature_flag_stack/__init__.py
  • backend/compact-connect/stacks/ingest_stack.py
  • backend/compact-connect/stacks/state_api_stack/v1_api/api_model.py

Comment thread backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts Outdated
Comment thread backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py Outdated

@coderabbitai coderabbitai Bot left a comment

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.

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/lambdas/python/common/cc_common/data_model/data_client.py (1)

2890-2894: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale "first transaction batch" claim contradicts the actual final-phase fence placement.

This docstring says the old-provider fence write is "executed in the first transaction batch," but the detailed phase comments a few lines below (2947-2966) and the actual code path (create_transaction_itemsdelete_transaction_itemsfinal_transaction_items) show the fence is bundled into the last phase together with the target license delete — that's precisely what makes large migrations replay-safe (both critical records survive together until the very end). Update the docstring so it doesn't contradict the phase-order explanation just below it.

📝 Proposed fix
-        Concurrency: the write against the old top-level provider record is conditioned on the dateOfUpdate
-        read at the start of the migration and executed in the first transaction batch. A concurrent migration
-        for the same old provider will fail that condition before writing anything, and its SQS retry re-reads
-        current state. The targeted license's delete is executed in the last batch so a crash mid-migration
-        leaves the license in place for the replay's idempotency guard to find; all other writes are idempotent.
+        Concurrency: the write against the old top-level provider record is conditioned on the dateOfUpdate
+        read at the start of the migration and is bundled, together with the targeted license's delete, into
+        the final transaction batch. A concurrent migration for the same old provider will fail that condition
+        before writing anything, and its SQS retry re-reads current state. Keeping the fence and the license
+        delete in the same final batch means a crash mid-migration leaves both records in place for the
+        replay's idempotency guard to find; all other writes are idempotent.
🤖 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/lambdas/python/common/cc_common/data_model/data_client.py`
around lines 2890 - 2894, Update the migration docstring around
create_transaction_items, delete_transaction_items, and final_transaction_items
to remove the claim that the old-provider fence runs in the first transaction
batch. Describe the fence as executing in the final phase together with the
targeted license delete, while preserving the replay-safety and concurrency
behavior described by the surrounding phase comments.
🤖 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.

Outside diff comments:
In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`:
- Around line 2890-2894: Update the migration docstring around
create_transaction_items, delete_transaction_items, and final_transaction_items
to remove the claim that the old-provider fence runs in the first transaction
batch. Describe the fence as executing in the final phase together with the
targeted license delete, while preserving the replay-safety and concurrency
behavior described by the surrounding phase comments.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29cf2775-6597-406a-9642-a4d51e4135bb

📥 Commits

Reviewing files that changed from the base of the PR and between 2bd5ea7 and 010d093.

📒 Files selected for processing (5)
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py
  • backend/compact-connect/lambdas/python/common/tests/function/test_data_client_ssn_correction.py
  • backend/compact-connect/lambdas/python/provider-data-v1/handlers/ingest.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py
  • backend/compact-connect/stacks/ingest_stack.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • backend/compact-connect/stacks/ingest_stack.py
  • backend/compact-connect/lambdas/python/provider-data-v1/handlers/ingest.py
  • backend/compact-connect/lambdas/python/common/tests/function/test_data_client_ssn_correction.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

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/lambdas/python/provider-data-v1/handlers/licenses.py (1)

8-22: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Read LICENSE_SSN_CORRECTION_MIGRATION_FLAG at invocation time Module-level caching freezes the value for warm Lambda containers, so a flag flip or fail-closed startup result can stay stuck until a cold start. Since this controls record-migration deletes, fetch it inside the handler or add a short TTL cache in both licenses.py and bulk_upload.py.

🤖 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/lambdas/python/provider-data-v1/handlers/licenses.py`
around lines 8 - 22, Move evaluation of LICENSE_SSN_CORRECTION_MIGRATION_FLAG
out of module scope and into the relevant handler flow so each invocation
observes current flag state while retaining fail_default=False. Apply the same
change in
backend/compact-connect/lambdas/python/provider-data-v1/handlers/licenses.py at
lines 8-22 and
backend/compact-connect/lambdas/python/provider-data-v1/handlers/bulk_upload.py
at lines 29-32; no module-level cached value should gate record migration
deletes.
🤖 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/provider-data-v1/handlers/bulk_upload.py`:
- Around line 184-189: Move the ssn_correction_migration_flag_enabled check and
validated_license.pop('previousSSN', None) logic before the schema.load(...)
call in the bulk upload validation flow. Keep the existing disabled-feature log
message and ensure previousSSN is removed from the input before schema
validation, while preserving it for enabled migrations.

In `@backend/compact-connect/tests/smoke/ssn_migration_smoke_tests.py`:
- Around line 599-603: Update the restoration flow around
_query_provider_ids_by_name and _restore_test_provider_account so it never
selects current_ids[0] based on unspecified search ordering. Track and use the
authoritative migration provider ID, or validate each candidate against DynamoDB
and choose the one matching the expected provider record, while retaining
original_provider_id only when no valid candidate exists.
- Around line 128-134: The resource setup in _create_test_app_client_headers and
the affected tests must clean up partial acquisitions when later setup fails.
Track each created app client and staff user immediately, move staff-user
creation inside the cleanup scope, and use nested try/finally or ExitStack so
failed token acquisition or subsequent setup removes every resource already
created while preserving normal test cleanup.
- Around line 555-594: The migration smoke tests only verify record movement,
not persisted SSN metadata. Update the full migration and roundtrip assertions
near _verify_all_records_migrated, plus the partial migration checks around the
corresponding test section, to inspect each target license’s ssnLastFour and
assert 8877 after full migration, 0000 after roundtrip, and 8899/8888 for the
partial target/source licenses.
- Around line 366-373: Update the Cognito/provider-record recovery logic around
the existing ClientError handler to evaluate the two states independently,
covering all four combinations of Cognito user presence and provider-record
presence. When a Cognito user already exists, read and validate its immutable
custom:providerId against provider_id before treating it as valid; reject or
recover mismatches. Restore whichever missing record or user is needed, while
preserving the no-op only when both records exist and the binding matches.

---

Outside diff comments:
In
`@backend/compact-connect/lambdas/python/provider-data-v1/handlers/licenses.py`:
- Around line 8-22: Move evaluation of LICENSE_SSN_CORRECTION_MIGRATION_FLAG out
of module scope and into the relevant handler flow so each invocation observes
current flag state while retaining fail_default=False. Apply the same change in
backend/compact-connect/lambdas/python/provider-data-v1/handlers/licenses.py at
lines 8-22 and
backend/compact-connect/lambdas/python/provider-data-v1/handlers/bulk_upload.py
at lines 29-32; no module-level cached value should gate record migration
deletes.
🪄 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: 99eef7f7-3ec0-41fb-acc2-092d8780894c

📥 Commits

Reviewing files that changed from the base of the PR and between 010d093 and 3a146d4.

📒 Files selected for processing (9)
  • backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts
  • backend/compact-connect/lambdas/python/provider-data-v1/handlers/bulk_upload.py
  • backend/compact-connect/lambdas/python/provider-data-v1/handlers/licenses.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_bulk_upload.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_licenses.py
  • backend/compact-connect/tests/smoke/README.md
  • backend/compact-connect/tests/smoke/config.py
  • backend/compact-connect/tests/smoke/smoke_tests_env_example.json
  • backend/compact-connect/tests/smoke/ssn_migration_smoke_tests.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_licenses.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_bulk_upload.py
  • backend/compact-connect/lambdas/nodejs/lib/email/email-notification-service.ts

Comment thread backend/compact-connect/tests/smoke/ssn_migration_smoke_tests.py Outdated
Comment thread backend/compact-connect/tests/smoke/ssn_migration_smoke_tests.py Outdated
Comment thread backend/compact-connect/tests/smoke/ssn_migration_smoke_tests.py
Comment thread backend/compact-connect/tests/smoke/ssn_migration_smoke_tests.py Outdated
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.

JCC - Migrate practitioner records from one SSN to another using 'previousSSN' field

1 participant