Skip to content

Update Provider top-level 'providerDateOfUpdate' timestamps when privileges are modified#1331

Merged
isabeleliassen merged 15 commits intocsg-org:mainfrom
InspiringApps:fix/update-timestamps
Apr 7, 2026
Merged

Update Provider top-level 'providerDateOfUpdate' timestamps when privileges are modified#1331
isabeleliassen merged 15 commits intocsg-org:mainfrom
InspiringApps:fix/update-timestamps

Conversation

@landonshumway-ia
Copy link
Copy Markdown
Collaborator

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

In order for states to programmatically poll for providers that have privileges within their state through the State 'two-way' API, the system relies on a 'providerDateOfUpdate' field to be updated on the provider record whenever a change occurs on their record. Currently, when a privilege is purchased, we are updating several fields on the provider record, but not the 'providerDateOfUpdate' field, resulting in providers not being returned in the API when states go to query for providers, unless they have another update take place from somewhere else in the system.

A state reported an issue where a provider purchased privileges in their state on 3/15, but because that individual has not had any other updates to their records, their 'providerDateOfUpdate' field is set to 02/26 before the privilege was purchased. Which means that the expected provider is not returned from the 'two-way' API within the expected time range.

This updates all the locations where we are performing updates that impact the privilege status so that we also update the 'providerDateOfUpdate' field on the top-level provider record so that the provider will be returned in the 'two-way' API when states query for provider updates.

Requirements List

  • This fix is a backwards compatible change. It involves an internal change of updating the 'providerDateOfUpdate' and 'dateOfUpdate' fields on provider records when updates occur. Those fields are only used functionally to search for users by their latest update time in the state API (it was previously used by the deprecated staff user search, which has been superseded by the OpenSearch provider search functionality and is no longer referenced by the UI).

Description List

  • Update data_client DB methods that were modifying provider records to now include updates to providerDateOfUpdate' and 'dateOfUpdate' timestamp fields to the time that the update occurs.

Closes #1330

Summary by CodeRabbit

  • Bug Fixes

    • Provider records now consistently update both system and provider update timestamps (using a single consistent timestamp) across privilege, encumbrance, military-affiliation, home-jurisdiction, email, and purchase flows; empty jurisdiction inputs now produce an error.
  • Tests

    • Added/extended tests asserting both timestamp fields are updated for privilege deactivation, encumbrance/lifting, military-affiliation changes, home-jurisdiction changes, email updates, and purchases.
  • Chores

    • Bumped pinned dev/runtime dependency versions across multiple backend components.

In order for states to programmatically poll for providers
that have privileges within their state through the State
'two-way' API, the system relies on a 'providerDateOfUpdate'
field to be updated on the provider record whenever a change
occurs on their record. Currently, when a privilege is
purchased, we are updating several fields on the provider
record, but not the 'providerDateOfUpdate' field, resulting in
providers not being returned in the API when states go to
query for providers, unless they have another update take
place from somewhere else in the system.

A state reported an issue where a provider purchased privileges
in their state on 3/15, but because that individual has not had
any other updates to their records, their 'providerDateOfUpdate'
field is set to 02/26 before the privilege was purchased. Which
means that the expected provider is not returned from the 'two-way'
API within the expected time range.

This updates all the locations where we are performing updates that impact the privilege
status so that we also update the 'providerDateOfUpdate' field on the top-level provider
record so that the provider will be returned in the 'two-way' API when states query for
provider updates.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

Top-level provider-record update paths were changed to set providerDateOfUpdate alongside dateOfUpdate, a single now timestamp is captured for grouped updates, provider Update items add ConditionExpression: attribute_exists(pk), create_provider_privileges now rejects empty jurisdictions, and multiple tests and dependency pins were added/updated.

Changes

Cohort / File(s) Summary
Data Client Core Logic
backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py
Add providerDateOfUpdate to provider UpdateExpressions and transaction items, capture a single now = config.current_standard_datetime for grouped updates, add ConditionExpression: attribute_exists(pk) to provider Update items, and validate non-empty jurisdiction_postal_abbreviations in create_provider_privileges.
Privilege Deactivation & Home-Jurisdiction Tests
backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py, backend/compact-connect/lambdas/python/data-events/tests/function/test_home_jurisdiction_events.py
Add/adjust tests to assert both dateOfUpdate and providerDateOfUpdate are set after privilege deactivation and home-jurisdiction changes (no-known-license path).
Encumbrance / Lifting Tests
backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py
Make provider dateOfUpdate seedable in test helpers; add tests asserting both top-level timestamps update on encumbrance actions and when the last encumbrance is lifted.
Provider Users (Military & Email) Tests
backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users.py, .../test_provider_users_email.py
Add tests verifying providerDateOfUpdate is set when ending military affiliation and when completing provider email update.
Purchase Privileges Tests
backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py
Add test (patching PurchaseClient) asserting dateOfUpdate and providerDateOfUpdate are updated after privilege purchase.
Smoke Tests & Helpers
backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py, backend/compact-connect/tests/smoke/smoke_common.py
Fix smoke helper license keys and dynamic fields; expand retry logic for GET /v1/provider-users/me to retry on 401 or 403.
Data Client Unit Tests Adjustments
backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py
Seed explicit provider records in tests and switch created privilege/license jurisdictions to shared constants where applicable.
Dependency Pin Updates
multiple requirements*.txt, backend/compact-connect/lambdas/python/common/requirements-dev.in
Bump numerous dev/runtime dependency pins across modules (boto3/botocore, aws-lambda-powertools, requests, certifi, moto, werkzeug, grpc/protobuf, add attrs constraint in requirements-dev.in, etc.).

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • jlkravitz

Poem

🐇

I hopped through rows of timestamps bright,
I nudged two fields so both catch light,
When homes, purchases, emails renew,
Both provider dates now tick anew,
A tiny hop to set time right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% 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 'Update Provider top-level 'providerDateOfUpdate' timestamps when privileges are modified' clearly and specifically describes the main change in the changeset: updating providerDateOfUpdate timestamps across multiple privilege-related operations.
Description check ✅ Passed The PR description is comprehensive and addresses the template requirements, including the problem statement, requirements list, description of changes, and testing guidance.
Linked Issues check ✅ Passed The PR implementation comprehensively addresses issue #1330 by updating providerDateOfUpdate and dateOfUpdate across all privilege-related code paths including purchases, home jurisdiction changes, encumbrances, military updates, and email updates.
Out of Scope Changes check ✅ Passed All code changes are directly related to updating providerDateOfUpdate and dateOfUpdate timestamps in privilege-related operations. Dependency version updates are routine maintenance and unrelated scope creep is not present.

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

✨ 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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py (1)

307-339: Consider reusing _request_deactivation_with_scopes in this new test.

The request assembly here largely duplicates existing helper logic; reusing the helper would keep setup consistent and reduce drift risk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py`
around lines 307 - 339, The test
test_deactivate_privilege_updates_provider_date_of_update duplicates request
assembly logic; replace the manual event construction with a call to the
existing helper _request_deactivation_with_scopes to keep setup consistent:
locate the test method and remove the manual manipulation of
event['requestContext']['authorizer']['claims']['scope'], ['sub'],
event['pathParameters'] and event['body'] and instead call
_request_deactivation_with_scopes(...) (the helper that builds the event with
scopes, sub, pathParameters and body) passing the same scope string 'openid
email aslp/admin', TEST_STAFF_USER_ID, DEFAULT_PROVIDER_ID and TEST_NOTE so the
rest of the test (calling deactivate_privilege and assertions) remains
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`:
- Around line 910-917: The current fix updates providerDateOfUpdate only in
end_military_affiliation(), but other update paths still set only dateOfUpdate;
update the DynamoDB UpdateExpression and ExpressionAttributeValues in
complete_military_affiliation_initialization() and process_military_audit() so
they also set providerDateOfUpdate to
self.config.current_standard_datetime.isoformat() (add providerDateOfUpdate to
the SET clause and add ':providerDateOfUpdate' to ExpressionAttributeValues),
ensuring all write paths keep providerDateOfUpdate in sync with dateOfUpdate.
- Around line 910-917: The UpdateExpression blocks currently call
self.config.current_standard_datetime.isoformat() twice, risking different
timestamps; capture the timestamp once into a local variable (e.g., ts =
self.config.current_standard_datetime.isoformat()) in the method where the
DynamoDB update is built and use that single ts for both ':dateOfUpdate' and
':providerDateOfUpdate' ExpressionAttributeValues (and anywhere ':dateOfUpdate'
/ ':providerDateOfUpdate' pairs are set). Apply the same single-capture pattern
to the other affected update sites mentioned (the similar blocks that set
':dateOfUpdate' and ':providerDateOfUpdate' at the other occurrences) so both
attributes always receive the identical timestamp.
- Around line 1588-1600: The provider item update currently only sets
providerDateOfUpdate when provider_encumbered_status changes, so child-record
encumbrance changes can leave the top-level provider timestamp stale and break
get_providers_sorted_by_updated(); modify the update logic around
provider_data.serialize_to_database_record() to always write a provider-only
timestamp update (set providerDateOfUpdate = current_standard_datetime) for
every encumbrance/lift transaction, and when provider_encumbered_status actually
changes fold that status change into the same UpdateExpression (e.g., set
providerDateOfUpdate = :providerDateOfUpdate, dateOfUpdate = :dateOfUpdate and
conditionally include encumberedStatus = :status); ensure
ExpressionAttributeValues include the timestamp value and status only when used
so the provider item is always touched even if its summary status is unchanged.

In
`@backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py`:
- Around line 1249-1272: The test
test_post_purchase_privileges_updates_provider_date_of_update is currently
relying on setup (process_registration_values) having already set dateOfUpdate
to the expected value; before invoking post_purchase_privileges you should seed
the provider item with an older timestamp so the update is meaningful: use the
test helper _provider_table to update the provider record (pk
f'{TEST_COMPACT}#PROVIDER#{TEST_PROVIDER_ID}', sk f'{TEST_COMPACT}#PROVIDER')
and set dateOfUpdate and providerDateOfUpdate to an earlier value (e.g.,
2000-01-01T00:00:00+00:00) in setUp or just before calling
post_purchase_privileges, then assert that post_purchase_privileges changed them
to the expected_datetime.

---

Nitpick comments:
In
`@backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py`:
- Around line 307-339: The test
test_deactivate_privilege_updates_provider_date_of_update duplicates request
assembly logic; replace the manual event construction with a call to the
existing helper _request_deactivation_with_scopes to keep setup consistent:
locate the test method and remove the manual manipulation of
event['requestContext']['authorizer']['claims']['scope'], ['sub'],
event['pathParameters'] and event['body'] and instead call
_request_deactivation_with_scopes(...) (the helper that builds the event with
scopes, sub, pathParameters and body) passing the same scope string 'openid
email aslp/admin', TEST_STAFF_USER_ID, DEFAULT_PROVIDER_ID and TEST_NOTE so the
rest of the test (calling deactivate_privilege and assertions) remains
unchanged.
🪄 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: 535a8690-e228-4658-8f99-25fde01abbff

📥 Commits

Reviewing files that changed from the base of the PR and between 6743691 and 02c712c.

📒 Files selected for processing (6)
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py
  • backend/compact-connect/lambdas/python/data-events/tests/function/test_home_jurisdiction_events.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users.py
  • backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`:
- Around line 530-536: The timestamp variable now is defined inside the
jurisdiction loop but later reused for the provider-level update (the block
constructing 'UpdateExpression' and 'ExpressionAttributeValues' containing
':dateOfUpdate' and ':providerDateOfUpdate'), which causes UnboundLocalError if
the loop never ran; fix by assigning now = datetime.datetime.utcnow() (or
equivalent) before entering the jurisdiction loop, and add a guard that fails
fast (raise ValueError or return with an explicit error) if
jurisdiction_postal_abbreviations is empty prior to building the transaction so
the provider-level update never references an undefined now.
🪄 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: 6af0ec25-2b2a-4c7f-a17d-97c19644d4a8

📥 Commits

Reviewing files that changed from the base of the PR and between 8208df8 and 2961910.

📒 Files selected for processing (3)
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_email.py
  • backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py

@ChiefStief
Copy link
Copy Markdown
Collaborator

This test is failing for me locally but doesnt seem to be here so not sure whats going on there: test_compact_admin_can_deactivate_privilege

Copy link
Copy Markdown
Collaborator

@ChiefStief ChiefStief left a comment

Choose a reason for hiding this comment

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

Looks good to me, I think the outstanding code rabbit comment is correct and I had the one test failing but other than that, ready to go.

@landonshumway-ia
Copy link
Copy Markdown
Collaborator Author

@ChiefStief I am not able to reproduce the test failure for 'test_compact_admin_can_deactivate_privilege'. Everything looks good there so it is likely a local environment problem that we can take a look at later. I have also run our smoke test suite against the endpoints that were modified and verified the tests continue to pass.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

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

253-265: ⚠️ Potential issue | 🟡 Minor

Incorrect state referenced in comments: 'ar' is Arkansas, not Alabama.

The postal abbreviation 'ar' corresponds to Arkansas, not Alabama (which is 'al'). The docstring and inline comment both incorrectly reference Alabama.

📝 Proposed fix to correct the comments
-    NOTE: this test assumes that the compact your test provider is registered in has Alabama as a live state in
-    its configuration, if that state is not live in your environment for that compact, you will need to add it or
+    NOTE: this test assumes that the compact your test provider is registered in has Arkansas as a live state in
+    its configuration, if that state is not live in your environment for that compact, you will need to add it or
     change the jurisdiction
     """
     logger.info('Running home jurisdiction change test - changing to jurisdiction with valid license')

     # Get the provider's information before making any changes
     provider_info_before = call_provider_users_me_endpoint()

     original_jurisdiction = provider_info_before.get('currentHomeJurisdiction')
     original_expiration_date = provider_info_before['licenses'][0]['dateOfExpiration']
-    new_jurisdiction = 'ar'  # Alabama - assuming the provider doesn't have a license here
+    new_jurisdiction = 'ar'  # Arkansas - assuming the provider doesn't have a license here
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py`
around lines 253 - 265, The comment and variable in the home jurisdiction change
smoke test incorrectly reference Alabama but use the Arkansas postal code;
update the test so the docstring and inline comment match the actual postal code
by changing the new_jurisdiction value from 'ar' to 'al' and update any
surrounding text that says "Alabama" or "assuming the provider doesn't have a
license here" to accurately reflect the chosen state; look for the
new_jurisdiction variable and the test docstring/comment in
home_jurisdiction_change_smoke_tests.py to make these edits.
♻️ Duplicate comments (1)
backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py (1)

443-467: ⚠️ Potential issue | 🟡 Minor

Pass the shared batch timestamp into _generate_privilege_record().

now is only reused for the privilege update rows and the top-level provider update. _generate_privilege_record() still calls config.current_standard_datetime internally for dateOfRenewal and new dateOfIssuance, so one purchase transaction can still persist slightly different timestamps across the privilege rows versus the provider/update rows.

💡 Suggested change
-                privilege_record: PrivilegeData = self._generate_privilege_record(
+                privilege_record: PrivilegeData = self._generate_privilege_record(
                     compact=compact,
                     provider_id=provider_id,
                     jurisdiction_postal_abbreviation=postal_abbreviation,
                     license_expiration_date=license_expiration_date,
                     compact_transaction_id=compact_transaction_id,
                     attestations=attestations,
                     license_type=license_type,
                     license_jurisdiction=license_jurisdiction,
                     original_privilege=original_privilege,
+                    current_datetime=now,
                 )

Update _generate_privilege_record() to accept and use current_datetime instead of reading the clock again.

Based on learnings: in `create_provider_privileges`, a single captured timestamp should be reused so the privilege records, privilege update records, and top-level provider update share the same timestamp within one transaction.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`
around lines 443 - 467, The code captures now in create_provider_privileges but
still lets _generate_privilege_record call config.current_standard_datetime,
causing inconsistent timestamps; update _generate_privilege_record signature to
accept a current_datetime (or current_standard_datetime) parameter, change its
internal uses of config.current_standard_datetime for dateOfRenewal and new
dateOfIssuance to use that parameter, and in create_provider_privileges pass the
captured now into the _generate_privilege_record(...) call so privilege records,
privilege update rows, and the provider update all share the same timestamp.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`:
- Around line 1502-1515: The Update operations that target top-level provider
records (sk == f'{compact}#PROVIDER') must include a condition to prevent
accidental upserts; add "ConditionExpression": "attribute_exists(pk)" to the
Update dicts in the four places named: create_provider_privileges,
deactivate_privilege, _generate_set_provider_encumbered_status_item, and
_get_provider_record_transaction_items_for_jurisdiction_with_no_known_license so
the DynamoDB Update fails if the provider pk does not already exist (apply the
same pattern used elsewhere in the file).

---

Outside diff comments:
In `@backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py`:
- Around line 253-265: The comment and variable in the home jurisdiction change
smoke test incorrectly reference Alabama but use the Arkansas postal code;
update the test so the docstring and inline comment match the actual postal code
by changing the new_jurisdiction value from 'ar' to 'al' and update any
surrounding text that says "Alabama" or "assuming the provider doesn't have a
license here" to accurately reflect the chosen state; look for the
new_jurisdiction variable and the test docstring/comment in
home_jurisdiction_change_smoke_tests.py to make these edits.

---

Duplicate comments:
In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`:
- Around line 443-467: The code captures now in create_provider_privileges but
still lets _generate_privilege_record call config.current_standard_datetime,
causing inconsistent timestamps; update _generate_privilege_record signature to
accept a current_datetime (or current_standard_datetime) parameter, change its
internal uses of config.current_standard_datetime for dateOfRenewal and new
dateOfIssuance to use that parameter, and in create_provider_privileges pass the
captured now into the _generate_privilege_record(...) call so privilege records,
privilege update rows, and the provider update all share the same timestamp.
🪄 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: de1ed5f9-9838-4968-a9e3-a8c49b1659b4

📥 Commits

Reviewing files that changed from the base of the PR and between 2961910 and 52f9b80.

📒 Files selected for processing (3)
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_email.py
  • backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_email.py

Copy link
Copy Markdown
Collaborator

@jlkravitz jlkravitz left a comment

Choose a reason for hiding this comment

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

few small comments + plus one or two unresolved coderabbit comments!

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.

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)

443-467: ⚠️ Potential issue | 🟡 Minor

Use the batch now for the privilege rows too.

Line 443 hoists a single purchase timestamp, but _generate_privilege_record() still re-reads config.current_standard_datetime for dateOfRenewal and new dateOfIssuance. If the loop crosses a second boundary, the privilege rows drift from the provider/update rows this refactor is trying to keep aligned.

♻️ Suggested change
-    def _generate_privilege_record(
+    def _generate_privilege_record(
         self,
         compact: str,
         provider_id: str,
         jurisdiction_postal_abbreviation: str,
         license_expiration_date: date,
         compact_transaction_id: str,
         attestations: list[dict],
         license_type: str,
         license_jurisdiction: str,
+        current_datetime: datetime,
         original_privilege: PrivilegeData | None = None,
     ) -> PrivilegeData:
-        current_datetime = config.current_standard_datetime
         try:
             license_type_abbreviation = self.config.license_type_abbreviations[compact][license_type]
             ...
                 privilege_record: PrivilegeData = self._generate_privilege_record(
                     compact=compact,
                     provider_id=provider_id,
                     jurisdiction_postal_abbreviation=postal_abbreviation,
                     license_expiration_date=license_expiration_date,
                     compact_transaction_id=compact_transaction_id,
                     attestations=attestations,
                     license_type=license_type,
                     license_jurisdiction=license_jurisdiction,
+                    current_datetime=now,
                     original_privilege=original_privilege,
                 )
Based on learnings, `create_provider_privileges` should reuse one captured timestamp across privilege records, privilege update records, and the top-level provider update within the same transaction.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`
around lines 443 - 467, The loop in create_provider_privileges captures a single
timestamp (now) but _generate_privilege_record still reads
config.current_standard_datetime internally, causing timestamps to drift; modify
_generate_privilege_record to accept a timestamp argument (e.g., captured_now or
batch_now) and use that for dateOfRenewal and dateOfIssuance (and any other uses
of config.current_standard_datetime), then pass the already-captured now from
create_provider_privileges into each call so all privilege rows and the
provider/update rows share the exact same timestamp.
♻️ Duplicate comments (1)
backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py (1)

533-546: ⚠️ Potential issue | 🟠 Major

Guard top-level provider Updates with attribute_exists(pk).

This provider write is still unconditional. If the top-level provider row is missing or concurrently deleted, DynamoDB will upsert a skeletal #PROVIDER item and the transaction will succeed with corrupted state instead of failing fast. The same pattern still exists at Line 1504, Line 1604, and Line 3009.

🛡️ Suggested change
                 'Update': {
                     'TableName': self.config.provider_table_name,
                     'Key': {
                         'pk': {'S': f'{compact}#PROVIDER#{provider_id}'},
                         'sk': {'S': f'{compact}#PROVIDER'},
                     },
                     'UpdateExpression': 'ADD `#privilegeJurisdictions` :newJurisdictions '
                     'SET dateOfUpdate = :dateOfUpdate, providerDateOfUpdate = :providerDateOfUpdate',
+                    'ConditionExpression': 'attribute_exists(pk)',
                     'ExpressionAttributeNames': {'#privilegeJurisdictions': 'privilegeJurisdictions'},
                     'ExpressionAttributeValues': {
                         ':newJurisdictions': {'SS': jurisdiction_postal_abbreviations},
                         ':dateOfUpdate': {'S': now.isoformat()},
                         ':providerDateOfUpdate': {'S': now.isoformat()},
Based on learnings, expected provider-record writes in this codebase should fail fast on missing provider rows because that represents a data consistency bug, not something to silently tolerate.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`
around lines 533 - 546, The Update call that writes the top-level provider item
(the dict using self.config.provider_table_name with Key
pk=f'{compact}#PROVIDER#{provider_id}' and sk=f'{compact}#PROVIDER') must be
made conditional so it fails if the provider row is missing; add a
ConditionExpression like "attribute_exists(pk)" (and/or include
attribute_exists(sk) for extra safety) to that Update request so the transaction
aborts instead of upserting a skeletal item, and apply the same change to the
other provider Update occurrences referenced (the similar Update blocks at the
other locations cited).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`:
- Around line 443-467: The loop in create_provider_privileges captures a single
timestamp (now) but _generate_privilege_record still reads
config.current_standard_datetime internally, causing timestamps to drift; modify
_generate_privilege_record to accept a timestamp argument (e.g., captured_now or
batch_now) and use that for dateOfRenewal and dateOfIssuance (and any other uses
of config.current_standard_datetime), then pass the already-captured now from
create_provider_privileges into each call so all privilege rows and the
provider/update rows share the exact same timestamp.

---

Duplicate comments:
In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`:
- Around line 533-546: The Update call that writes the top-level provider item
(the dict using self.config.provider_table_name with Key
pk=f'{compact}#PROVIDER#{provider_id}' and sk=f'{compact}#PROVIDER') must be
made conditional so it fails if the provider row is missing; add a
ConditionExpression like "attribute_exists(pk)" (and/or include
attribute_exists(sk) for extra safety) to that Update request so the transaction
aborts instead of upserting a skeletal item, and apply the same change to the
other provider Update occurrences referenced (the similar Update blocks at the
other locations cited).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1220dc8f-8b83-4966-9fa7-0be20d7f1738

📥 Commits

Reviewing files that changed from the base of the PR and between 2961910 and 52f9b80.

📒 Files selected for processing (3)
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_email.py
  • backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py
✅ Files skipped from review due to trivial changes (1)
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_provider_users_email.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/compact-connect/tests/smoke/home_jurisdiction_change_smoke_tests.py

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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)

531-548: ⚠️ Potential issue | 🟠 Major

Rollback currently undoes the new existence check.

If this top-level provider Update fails on attribute_exists(pk), the except ClientError path still calls _rollback_privilege_transactions(), and that helper always Puts provider_record back. That recreates the missing provider item from cached state instead of surfacing the integrity failure this condition was added to catch.

🛡️ Suggested fix
 def _rollback_privilege_transactions(
     self,
     processed_transactions: list[dict],
     provider_record: ProviderData,
     license_type: str,
     existing_privileges_for_license_type: list[PrivilegeData],
 ):
     """Roll back successful privilege transactions after a failure."""
     rollback_transactions = []
+    provider_record_db = provider_record.serialize_to_database_record()
+    provider_update_key = {
+        'pk': {'S': provider_record_db['pk']},
+        'sk': {'S': provider_record_db['sk']},
+    }
@@
-        rollback_transactions.append(
-            {
-                'Put': {
-                    'TableName': self.config.provider_table_name,
-                    'Item': TypeSerializer().serialize(provider_record.serialize_to_database_record())['M'],
-                }
-            }
-        )
+        if any(item.get('Update', {}).get('Key') == provider_update_key for item in processed_transactions):
+            rollback_transactions.append(
+                {
+                    'Put': {
+                        'TableName': self.config.provider_table_name,
+                        'Item': TypeSerializer().serialize(provider_record_db)['M'],
+                    }
+                }
+            )

Based on learnings, when a provider record is expected to exist, a missing provider record should fail fast as a data-consistency issue rather than being recreated from cached state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`
around lines 531 - 548, The provider Update uses a conditional
'attribute_exists(pk)' but the except ClientError path always calls
_rollback_privilege_transactions() which unconditionally Put's provider_record;
change the error handling so that if the ClientError is a
ConditionalCheckFailedException originating from the provider Update (detect by
checking error.response['Error']['Code']=='ConditionalCheckFailedException'
and/or by inspecting the failing transaction matching the Update with
'attribute_exists(pk)'), do not call _rollback_privilege_transactions() to
re-create provider_record and instead re-raise or return an integrity error;
alternatively modify _rollback_privilege_transactions() to accept a flag (e.g.,
skip_put_provider) and pass skip_put_provider=True when the failure is the
attribute_exists(pk) check so the cached provider is not restored.
♻️ Duplicate comments (1)
backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py (1)

443-467: 🛠️ Refactor suggestion | 🟠 Major

Thread the shared now into _generate_privilege_record().

This loop now reuses one timestamp for the privilege update records and the top-level provider update, but each privilege record still reads config.current_standard_datetime inside _generate_privilege_record(). A single purchase can still persist slightly different timestamps across records that are meant to represent the same transaction.

♻️ Suggested fix
 def _generate_privilege_record(
     self,
     compact: str,
     provider_id: str,
     jurisdiction_postal_abbreviation: str,
     license_expiration_date: date,
     compact_transaction_id: str,
     attestations: list[dict],
     license_type: str,
     license_jurisdiction: str,
+    current_datetime: datetime,
     original_privilege: PrivilegeData | None = None,
 ) -> PrivilegeData:
-    current_datetime = config.current_standard_datetime
     try:
         license_type_abbreviation = self.config.license_type_abbreviations[compact][license_type]
@@
                 privilege_record: PrivilegeData = self._generate_privilege_record(
                     compact=compact,
                     provider_id=provider_id,
                     jurisdiction_postal_abbreviation=postal_abbreviation,
                     license_expiration_date=license_expiration_date,
                     compact_transaction_id=compact_transaction_id,
                     attestations=attestations,
                     license_type=license_type,
                     license_jurisdiction=license_jurisdiction,
+                    current_datetime=now,
                     original_privilege=original_privilege,
                 )

Based on learnings, in create_provider_privileges, now = config.current_standard_datetime should be assigned once before the jurisdiction loop so all privilege records, privilege update records, and the top-level provider record update share the same timestamp within a single transaction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`
around lines 443 - 467, create_provider_privileges reads
config.current_standard_datetime into now but _generate_privilege_record still
calls config.current_standard_datetime internally, causing inconsistent
timestamps; pass the already-read now into _generate_privilege_record (add a now
parameter) and use that value inside _generate_privilege_record instead of
calling config.current_standard_datetime, so all records created by
create_provider_privileges (privilege records, privilege update records, and
provider update) share the same timestamp; update call sites of
_generate_privilege_record within create_provider_privileges to supply the now
variable and adjust the _generate_privilege_record signature and internal usage
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`:
- Around line 531-548: The provider Update uses a conditional
'attribute_exists(pk)' but the except ClientError path always calls
_rollback_privilege_transactions() which unconditionally Put's provider_record;
change the error handling so that if the ClientError is a
ConditionalCheckFailedException originating from the provider Update (detect by
checking error.response['Error']['Code']=='ConditionalCheckFailedException'
and/or by inspecting the failing transaction matching the Update with
'attribute_exists(pk)'), do not call _rollback_privilege_transactions() to
re-create provider_record and instead re-raise or return an integrity error;
alternatively modify _rollback_privilege_transactions() to accept a flag (e.g.,
skip_put_provider) and pass skip_put_provider=True when the failure is the
attribute_exists(pk) check so the cached provider is not restored.

---

Duplicate comments:
In
`@backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py`:
- Around line 443-467: create_provider_privileges reads
config.current_standard_datetime into now but _generate_privilege_record still
calls config.current_standard_datetime internally, causing inconsistent
timestamps; pass the already-read now into _generate_privilege_record (add a now
parameter) and use that value inside _generate_privilege_record instead of
calling config.current_standard_datetime, so all records created by
create_provider_privileges (privilege records, privilege update records, and
provider update) share the same timestamp; update call sites of
_generate_privilege_record within create_provider_privileges to supply the now
variable and adjust the _generate_privilege_record signature and internal usage
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 50cea1a2-4b1a-484e-bd0a-f97596e15537

📥 Commits

Reviewing files that changed from the base of the PR and between 2adea90 and 87cfd06.

📒 Files selected for processing (1)
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py (1)

488-494: Minor formatting inconsistency.

The closing brace placement at line 493 (})) differs from the pattern used in the other test setups (e.g., lines 155-158 where }) is on a separate line). This is a nitpick and doesn't affect functionality.

🔧 Optional: Consistent formatting
         # add provider record
         self.test_data_generator.put_default_provider_record_in_provider_table(
-            value_overrides={
-            'providerId': provider_uuid,
-            'privilegeJurisdictions': {}
-        })
+            value_overrides={
+                'providerId': provider_uuid,
+                'privilegeJurisdictions': {}
+            }
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py`
around lines 488 - 494, Move the closing brace for the value_overrides dict onto
its own line to match the project’s test formatting pattern: update the call to
put_default_provider_record_in_provider_table so that the dictionary passed to
value_overrides (and the closing parenthesis) places the final "}" and ")" on
separate lines; locate the invocation of
put_default_provider_record_in_provider_table that uses value_overrides with
provider_uuid and adjust the brace/paren placement to mirror the other test
setups.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py`:
- Around line 488-494: Move the closing brace for the value_overrides dict onto
its own line to match the project’s test formatting pattern: update the call to
put_default_provider_record_in_provider_table so that the dictionary passed to
value_overrides (and the closing parenthesis) places the final "}" and ")" on
separate lines; locate the invocation of
put_default_provider_record_in_provider_table that uses value_overrides with
provider_uuid and adjust the brace/paren placement to mirror the other test
setups.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4040a43a-87db-4c02-9b66-dbb149230171

📥 Commits

Reviewing files that changed from the base of the PR and between 87cfd06 and 6e895b2.

📒 Files selected for processing (2)
  • backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py
  • backend/compact-connect/tests/smoke/smoke_common.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/compact-connect/tests/smoke/smoke_common.py

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py (1)

155-158: Use set() for empty privilegeJurisdictions overrides.

{} is an empty dict in Python, not an empty set. The privilegeJurisdictions field in the provider schema is defined with Set(String, load_default=set()), and all test assertions expect set objects. For consistency with the schema semantics and with other usage in this test file (e.g., line 526), use set() instead.

♻️ Suggested patch
-        provider_record = self.test_data_generator.put_default_provider_record_in_provider_table(
-            value_overrides={'privilegeJurisdictions': {}}
-        )
+        provider_record = self.test_data_generator.put_default_provider_record_in_provider_table(
+            value_overrides={'privilegeJurisdictions': set()}
+        )

-        self.test_data_generator.put_default_provider_record_in_provider_table(
-            value_overrides={'providerId': provider_uuid, 'privilegeJurisdictions': {}}
-        )
+        self.test_data_generator.put_default_provider_record_in_provider_table(
+            value_overrides={'providerId': provider_uuid, 'privilegeJurisdictions': set()}
+        )

-        self.test_data_generator.put_default_provider_record_in_provider_table(
-            value_overrides={'providerId': provider_uuid, 'privilegeJurisdictions': {}}
-        )
+        self.test_data_generator.put_default_provider_record_in_provider_table(
+            value_overrides={'providerId': provider_uuid, 'privilegeJurisdictions': set()}
+        )

Also applies to: 252-255, 485-488

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py`
around lines 155 - 158, The test is passing an empty dict for
privilegeJurisdictions but the provider schema and assertions expect a set;
update calls to put_default_provider_record_in_provider_table (and any similar
test_data_generator invocations that set 'privilegeJurisdictions') to use
value_overrides={'privilegeJurisdictions': set()} instead of {} (also fix the
two other occurrences in the same test file where {} is used for
privilegeJurisdictions).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py`:
- Around line 155-158: The test is passing an empty dict for
privilegeJurisdictions but the provider schema and assertions expect a set;
update calls to put_default_provider_record_in_provider_table (and any similar
test_data_generator invocations that set 'privilegeJurisdictions') to use
value_overrides={'privilegeJurisdictions': set()} instead of {} (also fix the
two other occurrences in the same test file where {} is used for
privilegeJurisdictions).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 577eda6d-7901-4cd8-9fb6-ff569ed56d81

📥 Commits

Reviewing files that changed from the base of the PR and between 6e895b2 and dcbd0f0.

📒 Files selected for processing (1)
  • backend/compact-connect/lambdas/python/common/tests/function/test_data_client.py

Copy link
Copy Markdown
Collaborator

@jlkravitz jlkravitz left a comment

Choose a reason for hiding this comment

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

@isabeleliassen This is good to merge. Once merged, I'll do a prod deploy.

@isabeleliassen isabeleliassen merged commit ddd218d into csg-org:main Apr 7, 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.

Bug - Provider top-level 'providerDateOfUpdate' timestamps are not updated when privileges are purchased.

4 participants