Skip to content

Feat/lift encumbrance #824

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: development
Choose a base branch
from

Conversation

landonshumway-ia
Copy link
Collaborator

@landonshumway-ia landonshumway-ia commented May 29, 2025

State admins need the ability to lift an encumbrance from a license or privilege when appropriate. This adds new endpoints for setting an effective lift date on license and privilege encumbrances.

Closes #678

Summary by CodeRabbit

  • New Features

    • Added support for lifting (removing) encumbrances from privilege and license records via new PATCH API endpoints.
    • Users can now submit requests to lift encumbrances, specifying the effective lift date and encumbrance ID.
    • The system updates privilege/license and provider statuses accordingly when all encumbrances are lifted.
  • Bug Fixes

    • Improved error messages and validation for encumbrance lifting requests.
  • Documentation

    • Added and updated API request schemas for privilege and license encumbrance lifting.
  • Tests

    • Introduced comprehensive unit and smoke tests covering encumbrance lifting workflows, validation, and authorization.
  • Refactor

    • Streamlined API resource handling and improved code structure for provider record utilities and test helpers.

Copy link
Contributor

coderabbitai bot commented May 29, 2025

Walkthrough

This change introduces the ability for state admins to lift encumbrances from specific license or privilege records via new PATCH API endpoints. It adds backend logic for transactional updates, new request models, API resource configurations, comprehensive unit and smoke tests, and supporting utilities to ensure proper encumbrance lifting, validation, and record state transitions.

Changes

File(s) Change Summary
data_model/data_client.py, provider_record_util.py Add ProviderUserRecords class and new methods to DataClient for lifting encumbrances, transactional DynamoDB updates, and record retrieval/filtering.
schema/common.py Add LIFTING_ENCUMBRANCE enum value to UpdateCategory.
handlers/encumbrance.py Add PATCH routing and handlers for lifting privilege/license encumbrances, with input validation and error handling.
tests/function/test_handlers/test_encumbrance.py Add extensive tests for PATCH privilege/license encumbrance lifting, covering validation, success, error, and access control scenarios.
api_stack/v1_api/api_model.py Add PATCH request models for privilege and license encumbrance lifting.
api_stack/v1_api/provider_management.py Add PATCH methods to privilege/license encumbrance API resources, using new models and validators.
tests/app/test_api/test_provider_management_api.py Add helpers and tests for PATCH encumbrance endpoints, refactor resource ID lookups, and snapshot model validation.
tests/resources/snapshots/PRIVILEGE_ENCUMBRANCE_LIFTING_REQUEST_SCHEMA.json, LICENSE_ENCUMBRANCE_LIFTING_REQUEST_SCHEMA.json Add JSON schemas for PATCH encumbrance lifting request validation.
tests/smoke/encumbrance_smoke_tests.py Add end-to-end smoke tests for license and privilege encumbrance lifting workflows, including setup and cleanup.
tests/smoke/smoke_common.py Add utility to retrieve all provider database records for smoke tests.

Sequence Diagram(s)

sequenceDiagram
    participant Admin as State Admin
    participant API as API Gateway
    participant Handler as Lambda Handler
    participant DataClient as DataClient
    participant DynamoDB as DynamoDB

    Admin->>API: PATCH /license/encumbrance or /privilege/encumbrance
    API->>Handler: Invoke handler with event
    Handler->>Handler: Validate input, parse lift date & encumbrance ID
    Handler->>DataClient: lift_license_encumbrance(...) or lift_privilege_encumbrance(...)
    DataClient->>DynamoDB: Retrieve provider user records
    DataClient->>DynamoDB: Transactional update (adverse action, license/privilege, provider)
    DynamoDB-->>DataClient: Transaction result
    DataClient-->>Handler: Success or error
    Handler-->>API: 200 OK or error response
    API-->>Admin: Response
Loading

Assessment against linked issues

Objective Addressed Explanation
Allow state admins to lift an encumbrance from a specific license or privilege, specifying an effective lift date (#678)
Use PATCH endpoints for lifting encumbrances (#678)
Automated tests for encumbrance lifting (#678)

Possibly related PRs

Suggested reviewers

  • jusdino
  • jlkravitz
  • isabeleliassen

Poem

🐇
Encumbrances lifted, the burden's released,
With PATCH and a date, the worries have ceased.
Licenses and privileges, now shining anew,
State admins rejoice—your power comes through!
Tests hop along, ensuring all's right,
In the land of the Compact, the future is bright!

✨ 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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@landonshumway-ia landonshumway-ia marked this pull request as ready for review May 29, 2025 21:58
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: 5

🧹 Nitpick comments (5)
backend/compact-connect/lambdas/python/common/cc_common/data_model/provider_record_util.py (1)

368-398: Consider using date objects directly for sorting.

While sorting by isoformat() works correctly for ISO dates, using the date objects directly would be more robust and avoid string conversion overhead.

-    key=lambda x: x.dateOfIssuance.isoformat(),
+    key=lambda x: x.dateOfIssuance,

This change applies to all three sorting operations in this method.

backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py (1)

781-781: Use consistent datetime parameter syntax.

For consistency with line 477, use the positional argument form.

-        future_date = (datetime.now(tz=UTC) + timedelta(days=2)).strftime('%Y-%m-%d')
+        future_date = (datetime.now(UTC) + timedelta(days=2)).strftime('%Y-%m-%d')
backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py (3)

165-198: Consider using the existing pagination pattern for consistency.

The method correctly implements pagination, but could be improved:

  1. Consider using the @paginated_query decorator pattern used elsewhere in this class for consistency
  2. Add exception handling for DynamoDB operations

Here's a more consistent approach using the existing pagination pattern:

-    @logger_inject_kwargs(logger, 'compact', 'provider_id')
-    def get_provider_user_records(
-        self,
-        *,
-        compact: str,
-        provider_id: str,
-        consistent_read: bool = True,
-    ) -> ProviderUserRecords:
-        logger.info('Getting provider')
-
-        resp = {'Items': []}
-        last_evaluated_key = None
-
-        while True:
-            pagination = {'ExclusiveStartKey': last_evaluated_key} if last_evaluated_key else {}
-
-            query_resp = self.config.provider_table.query(
-                Select='ALL_ATTRIBUTES',
-                KeyConditionExpression=Key('pk').eq(f'{compact}#PROVIDER#{provider_id}')
-                & Key('sk').begins_with(f'{compact}#PROVIDER'),
-                ConsistentRead=consistent_read,
-                **pagination,
-            )
-
-            resp['Items'].extend(query_resp.get('Items', []))
-
-            last_evaluated_key = query_resp.get('LastEvaluatedKey')
-            if not last_evaluated_key:
-                break
-        if not resp['Items']:
-            raise CCNotFoundException('Provider not found')
-
-        return ProviderUserRecords(resp['Items'])
+    @logger_inject_kwargs(logger, 'compact', 'provider_id')
+    def get_provider_user_records(
+        self,
+        *,
+        compact: str,
+        provider_id: str,
+        consistent_read: bool = True,
+    ) -> ProviderUserRecords:
+        logger.info('Getting provider')
+        
+        all_items = []
+        try:
+            # Use the existing get_provider method with detail=True to get all records
+            result = self.get_provider(
+                compact=compact,
+                provider_id=provider_id,
+                detail=True,
+                consistent_read=consistent_read
+            )
+            all_items.extend(result.get('Items', []))
+            
+            # Handle any remaining pages
+            while 'LastEvaluatedKey' in result:
+                result = self.get_provider(
+                    compact=compact,
+                    provider_id=provider_id,
+                    detail=True,
+                    consistent_read=consistent_read,
+                    dynamo_pagination={'ExclusiveStartKey': result['LastEvaluatedKey']}
+                )
+                all_items.extend(result.get('Items', []))
+                
+        except CCNotFoundException:
+            raise
+        except ClientError as e:
+            logger.error('Failed to retrieve provider records', error=str(e))
+            raise CCAwsServiceException('Failed to retrieve provider records') from e
+            
+        return ProviderUserRecords(all_items)

1100-1166: Refactor to reduce code duplication and improve type checking.

The logic is correct but can be improved:

  1. Using getattr for type checking (line 1121) is fragile
  2. The filtering logic is duplicated between licenses and privileges

Consider this refactored approach:

     def _generate_provider_encumbered_status_transaction_items_if_no_encumbrances(
         self, provider_user_records: ProviderUserRecords, lifted_record: PrivilegeData | LicenseData
     ) -> list[dict]:
         """
         Check if any licenses or privileges (excluding the lifted record) still have encumbered status.
         If none are encumbered, return transaction items to set the provider record to unencumbered.
 
         :param ProviderUserRecords provider_user_records: All provider records
         :param lifted_record: The privilege or license record that is having its encumbrance lifted
         :return: List of transaction items (empty if other records are still encumbered)
         """
         # Get the provider record
         provider_record = provider_user_records.get_provider_record()
 
         # Get all license records
         license_records = provider_user_records.get_license_records()
 
         # Get all privilege records
         privilege_records = provider_user_records.get_privilege_records()
 
-        # Check if the lifted record is a license or privilege based on its type
-        lifted_record_type = getattr(lifted_record, 'type', None)
+        # Determine if lifted record is a license or privilege
+        is_lifted_license = isinstance(lifted_record, LicenseData)
+        
+        def is_same_record(record, lifted):
+            """Check if a record matches the lifted record."""
+            return (record.jurisdiction == lifted.jurisdiction and 
+                    record.licenseType == lifted.licenseType)
 
         # Check license records for encumbered status (excluding the lifted record if it's a license)
         for license_record in license_records:
-            if (
-                lifted_record_type == 'license'
-                and license_record.jurisdiction == lifted_record.jurisdiction
-                and license_record.licenseType == lifted_record.licenseType
-            ):
+            if is_lifted_license and is_same_record(license_record, lifted_record):
                 # Skip the record being lifted
                 continue
             if license_record.encumberedStatus == LicenseEncumberedStatusEnum.ENCUMBERED:
                 logger.info(
                     'License record still encumbered, provider record will not be updated',
                     encumbered_license_jurisdiction=license_record.jurisdiction,
                     encumbered_license_type=license_record.licenseType,
                 )
                 return []
 
         # Check privilege records for encumbered status (excluding the lifted record if it's a privilege)
         for privilege_record in privilege_records:
-            if (
-                lifted_record_type == 'privilege'
-                and privilege_record.jurisdiction == lifted_record.jurisdiction
-                and privilege_record.licenseType == lifted_record.licenseType
-            ):
+            if not is_lifted_license and is_same_record(privilege_record, lifted_record):
                 # Skip the record being lifted
                 continue
             if privilege_record.encumberedStatus == PrivilegeEncumberedStatusEnum.ENCUMBERED:
                 logger.info(
                     'Privilege record still encumbered, provider record will not be updated',
                     encumbered_privilege_jurisdiction=privilege_record.jurisdiction,
                     encumbered_privilege_type=privilege_record.licenseType,
                 )
                 return []

1491-1624: Consider refactoring to reduce duplication with lift_privilege_encumbrance.

This method is nearly identical to lift_privilege_encumbrance. The same error handling issue exists at line 1563.

Consider extracting the common logic into a shared private method:

def _lift_encumbrance(
    self,
    compact: str,
    provider_id: str,
    jurisdiction: str,
    license_type_abbreviation: str,
    adverse_action_id: str,
    effective_lift_date: date,
    lifting_user: str,
    record_type: Literal['license', 'privilege'],
) -> None:
    """
    Generic method to lift an encumbrance from either a license or privilege record.
    """
    # Common implementation here...

This would eliminate the duplication and make maintenance easier.

Also apply the same error handling fix for the license type lookup as suggested for lift_privilege_encumbrance.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40f05e6 and f457d5c.

📒 Files selected for processing (12)
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py (6 hunks)
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/provider_record_util.py (2 hunks)
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/common.py (1 hunks)
  • backend/compact-connect/lambdas/python/provider-data-v1/handlers/encumbrance.py (4 hunks)
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py (2 hunks)
  • backend/compact-connect/stacks/api_stack/v1_api/api_model.py (1 hunks)
  • backend/compact-connect/stacks/api_stack/v1_api/provider_management.py (2 hunks)
  • backend/compact-connect/tests/app/test_api/test_provider_management_api.py (5 hunks)
  • backend/compact-connect/tests/resources/snapshots/LICENSE_ENCUMBRANCE_LIFTING_REQUEST_SCHEMA.json (1 hunks)
  • backend/compact-connect/tests/resources/snapshots/PRIVILEGE_ENCUMBRANCE_LIFTING_REQUEST_SCHEMA.json (1 hunks)
  • backend/compact-connect/tests/smoke/encumbrance_smoke_tests.py (1 hunks)
  • backend/compact-connect/tests/smoke/smoke_common.py (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
backend/compact-connect/tests/smoke/smoke_common.py (1)
backend/compact-connect/tests/smoke/config.py (1)
  • provider_user_dynamodb_table (20-21)
backend/compact-connect/tests/smoke/encumbrance_smoke_tests.py (3)
backend/compact-connect/tests/smoke/purchasing_privileges_smoke_tests.py (1)
  • test_purchasing_privilege (106-239)
backend/compact-connect/tests/smoke/smoke_common.py (9)
  • SmokeTestFailureException (12-18)
  • call_provider_users_me_endpoint (218-226)
  • create_test_staff_user (100-122)
  • delete_test_staff_user (82-97)
  • get_all_provider_database_records (229-239)
  • get_license_type_abbreviation (174-181)
  • get_provider_user_dynamodb_table (188-189)
  • get_staff_user_auth_headers (167-171)
  • load_smoke_test_env (212-215)
backend/compact-connect/tests/smoke/config.py (1)
  • api_base_url (16-17)
🔇 Additional comments (15)
backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/common.py (1)

226-226: LGTM! Clean enum addition for lifting encumbrance functionality.

The new LIFTING_ENCUMBRANCE enum member follows the established naming convention and appropriately represents the update category for lifting encumbrances.

backend/compact-connect/tests/smoke/smoke_common.py (2)

7-7: LGTM! Necessary import for DynamoDB query condition.

The Key import is required for the new get_all_provider_database_records() function.


229-239: LGTM! Well-implemented helper function for testing.

The get_all_provider_database_records() function provides a clean utility for retrieving all provider database records during testing. The implementation correctly:

  • Uses the existing call_provider_users_me_endpoint() to get provider context
  • Constructs the appropriate partition key format {compact}#PROVIDER#{provider_id}
  • Performs a DynamoDB query with proper key condition
  • Returns the query results

This will be valuable for validating encumbrance lifting functionality in smoke tests.

backend/compact-connect/stacks/api_stack/v1_api/provider_management.py (2)

634-650:

✅ Verification successful

LGTM! Well-structured PATCH endpoint for lifting privilege encumbrances.

The implementation correctly follows the established pattern of the existing POST method with appropriate authorization, validation, and response handling. Using the same Lambda handler (provider_encumbrance_handler) for both POST and PATCH operations is a good approach for code reuse.

Let me verify that the referenced request model exists and the Lambda handler properly routes PATCH requests:


🏁 Script executed:

#!/bin/bash
# Description: Verify the patch privilege encumbrance request model exists and Lambda handler supports PATCH routing

# Test: Search for the patch_privilege_encumbrance_request_model definition
echo "=== Searching for patch_privilege_encumbrance_request_model ==="
rg -A 5 "patch_privilege_encumbrance_request_model"

# Test: Search for PATCH method handling in the encumbrance handler
echo "=== Searching for PATCH method handling in encumbrance handler ==="
rg -A 10 -B 5 "PATCH|patch" --type py | grep -A 10 -B 5 "encumbrance"

Length of output: 59012


Verification complete – PATCH encumbrance endpoints are correctly wired

  • api_model.py defines patch_privilege_encumbrance_request_model and patch_license_encumbrance_request_model with the required effectiveLiftDate and encumbranceId fields.
  • handlers/encumbrance.py routes httpMethod == 'PATCH' to handle_privilege_encumbrance_lifting and handle_license_encumbrance_lifting.
  • Smoke and unit tests exercise both PATCH endpoints successfully.

No further changes needed.


676-692:

✅ Verification successful

LGTM! Consistent implementation for lifting license encumbrances.

The license encumbrance PATCH method implementation is consistent with the privilege encumbrance PATCH method, maintaining the same security, validation, and integration patterns. This consistency ensures predictable behavior across both encumbrance types.

Let me verify the corresponding request model for license encumbrances:


🏁 Script executed:

#!/bin/bash
# Description: Verify the patch license encumbrance request model exists

# Test: Search for the patch_license_encumbrance_request_model definition
echo "=== Searching for patch_license_encumbrance_request_model ==="
rg -A 5 "patch_license_encumbrance_request_model"

# Test: Verify consistency between privilege and license patch models
echo "=== Comparing privilege and license patch models ==="
rg -A 10 "patch_.*_encumbrance_request_model"

Length of output: 11688


LGTM – PATCH license encumbrance request model verified
Verified that the patch_license_encumbrance_request_model is defined in api_model.py and matches the patch_privilege_encumbrance_request_model schema (same required properties and structure). The PATCH method in provider_management.py is consistent with the privilege encumbrance implementation, preserving identical security, validation, and integration patterns.

backend/compact-connect/stacks/api_stack/v1_api/api_model.py (1)

443-503: LGTM! Well-structured request models for encumbrance lifting.

The implementation correctly follows the established patterns in the codebase with proper caching, validation schemas, and clear documentation. Both models appropriately validate the date format and enforce length constraints on the encumbrance ID.

backend/compact-connect/lambdas/python/provider-data-v1/handlers/encumbrance.py (1)

65-65: Good improvement to error messaging.

Including the detailed validation errors helps developers debug request issues more effectively.

backend/compact-connect/lambdas/python/common/cc_common/data_model/provider_record_util.py (1)

247-416: Well-designed utility class for provider record management.

The ProviderUserRecords class provides a clean abstraction for working with provider records, with appropriate separation of concerns and clear method documentation. The implementation follows good practices for data encapsulation and type safety.

backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py (1)

424-1031: Excellent test coverage for the new encumbrance lifting functionality!

The test classes TestPatchPrivilegeEncumbranceLifting and TestPatchLicenseEncumbranceLifting provide comprehensive coverage including:

  • Input validation (future dates, invalid date formats)
  • Error scenarios (not found, already lifted)
  • Successful lifting operations with proper state transitions
  • Authorization enforcement (only state admins allowed)
  • Edge cases (multiple encumbrances, mixed license/privilege encumbrances)

The use of helper methods reduces duplication and improves maintainability.

backend/compact-connect/tests/smoke/encumbrance_smoke_tests.py (1)

1-566: Well-structured end-to-end smoke tests for encumbrance functionality!

The smoke tests provide excellent coverage of the complete encumbrance lifecycle:

  • Proper test environment setup and cleanup
  • Comprehensive workflow testing (create → verify → lift → verify)
  • Good error handling with descriptive failure messages
  • Proper test isolation with user creation/deletion

The helper functions and clear logging make the tests maintainable and debuggable.

backend/compact-connect/tests/app/test_api/test_provider_management_api.py (2)

23-179: Excellent refactoring with helper methods!

The introduction of helper methods _get_privilege_encumbrance_resource_id, _get_privilege_license_type_param_resource_id, and _get_license_encumbrance_resource_id significantly improves code maintainability by:

  • Eliminating duplicate resource traversal logic
  • Making the tests more readable
  • Providing a single source of truth for resource hierarchy navigation

690-794: Comprehensive test coverage for the new PATCH endpoints!

The new tests test_synth_generates_privilege_encumbrance_lifting_endpoint and test_synth_generates_license_encumbrance_lifting_endpoint properly verify:

  • Correct HTTP method (PATCH)
  • Proper authorization configuration
  • Request model validation
  • Integration with the encumbrance handler
  • Snapshot testing for schema validation

These tests follow the established patterns and ensure the PATCH endpoints are configured correctly in the API Gateway.

backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py (3)

12-12: LGTM!

The import is properly placed and necessary for the new ProviderUserRecords functionality.


1017-1019: LGTM!

Clean helper method for generating Put transaction items.


1020-1049: LGTM!

Well-implemented method that correctly generates the Update transaction item for lifting an adverse action.

"effectiveLiftDate": {
"description": "The effective date when the encumbrance will be lifted",
"format": "date",
"pattern": "^[12]{1}[0-9]{3}-[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the date validation pattern to prevent invalid dates.

The current regex pattern ^[12]{1}[0-9]{3}-[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$ incorrectly allows invalid dates:

  • Month part [01]{1}[0-9]{1} accepts 00-19, but valid months are 01-12
  • Day part [0-3]{1}[0-9]{1} accepts 00-39, but valid days are 01-31 and 00 is invalid

Apply this fix to use a proper date validation pattern:

-      "pattern": "^[12]{1}[0-9]{3}-[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$",
+      "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$",

This pattern correctly validates:

  • Month: 01-12 using (0[1-9]|1[0-2])
  • Day: 01-31 using (0[1-9]|[12][0-9]|3[01])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"pattern": "^[12]{1}[0-9]{3}-[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$",
"pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$",
🤖 Prompt for AI Agents
In
backend/compact-connect/tests/resources/snapshots/PRIVILEGE_ENCUMBRANCE_LIFTING_REQUEST_SCHEMA.json
at line 7, the date regex pattern allows invalid months and days. Replace the
month part `[01]{1}[0-9]{1}` with `(0[1-9]|1[0-2])` to restrict months to 01-12,
and replace the day part `[0-3]{1}[0-9]{1}` with `(0[1-9]|[12][0-9]|3[01])` to
restrict days to 01-31, ensuring the pattern only matches valid dates.

"effectiveLiftDate": {
"description": "The effective date when the encumbrance will be lifted",
"format": "date",
"pattern": "^[12]{1}[0-9]{3}-[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the date validation pattern to prevent invalid dates.

The current regex pattern ^[12]{1}[0-9]{3}-[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$ incorrectly allows invalid dates:

  • Month part [01]{1}[0-9]{1} accepts 00-19, but valid months are 01-12
  • Day part [0-3]{1}[0-9]{1} accepts 00-39, but valid days are 01-31 and 00 is invalid

Apply this fix to use a proper date validation pattern:

-      "pattern": "^[12]{1}[0-9]{3}-[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$",
+      "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$",

This pattern correctly validates:

  • Month: 01-12 using (0[1-9]|1[0-2])
  • Day: 01-31 using (0[1-9]|[12][0-9]|3[01])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"pattern": "^[12]{1}[0-9]{3}-[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$",
"pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$",
🤖 Prompt for AI Agents
In
backend/compact-connect/tests/resources/snapshots/LICENSE_ENCUMBRANCE_LIFTING_REQUEST_SCHEMA.json
at line 7, the date validation regex pattern allows invalid months and days.
Replace the current pattern "^[12]{1}[0-9]{3}-[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$"
with a stricter pattern that validates months as 01-12 using "(0[1-9]|1[0-2])"
and days as 01-31 using "(0[1-9]|[12][0-9]|3[01])" to prevent invalid dates.

Comment on lines +116 to +201
def handle_privilege_encumbrance_lifting(event: dict) -> dict:
"""Handle lifting encumbrance from a privilege record"""
# Get the cognito sub of the caller for tracing
cognito_sub = _get_submitting_user_id(event)

with logger.append_context_keys(cognito_sub=cognito_sub):
logger.info('Processing privilege encumbrance lifting')

# Extract path parameters
compact = event['pathParameters']['compact']
provider_id = event['pathParameters']['providerId']
jurisdiction = event['pathParameters']['jurisdiction']
license_type_abbreviation = event['pathParameters']['licenseType'].lower()

# Parse and validate request body
body = json.loads(event['body'])
effective_lift_date = body['effectiveLiftDate']
encumbrance_id = body['encumbranceId']

# Validate date format and parse the effective lift date
try:
lift_date = datetime.fromisoformat(effective_lift_date).date()
except ValueError as e:
raise CCInvalidRequestException('Invalid date format. Expected ISO format (YYYY-MM-DD)') from e

current_date = config.expiration_resolution_date

if lift_date > current_date:
raise CCInvalidRequestException('The lift date must not be a future date')

# Call the data client method to lift the privilege encumbrance
config.data_client.lift_privilege_encumbrance(
compact=compact,
provider_id=provider_id,
jurisdiction=jurisdiction,
license_type_abbreviation=license_type_abbreviation,
adverse_action_id=encumbrance_id,
effective_lift_date=lift_date,
lifting_user=cognito_sub,
)

return {'message': 'OK'}


def handle_license_encumbrance_lifting(event: dict) -> dict:
"""Handle lifting encumbrance from a license record"""
# Get the cognito sub of the caller for tracing
cognito_sub = _get_submitting_user_id(event)

with logger.append_context_keys(cognito_sub=cognito_sub):
logger.info('Processing license encumbrance lifting')

# Extract path parameters
compact = event['pathParameters']['compact']
provider_id = event['pathParameters']['providerId']
jurisdiction = event['pathParameters']['jurisdiction']
license_type_abbreviation = event['pathParameters']['licenseType'].lower()

# Parse and validate request body
body = json.loads(event['body'])
effective_lift_date = body['effectiveLiftDate']
encumbrance_id = body['encumbranceId']

# Validate date format and parse the effective lift date
try:
lift_date = datetime.fromisoformat(effective_lift_date).date()
except ValueError as e:
raise CCInvalidRequestException('Invalid date format. Expected ISO format (YYYY-MM-DD)') from e

current_date = config.expiration_resolution_date

if lift_date > current_date:
raise CCInvalidRequestException('The lift date must not be a future date')

# Call the data client method to lift the license encumbrance
config.data_client.lift_license_encumbrance(
compact=compact,
provider_id=provider_id,
jurisdiction=jurisdiction,
license_type_abbreviation=license_type_abbreviation,
adverse_action_id=encumbrance_id,
effective_lift_date=lift_date,
lifting_user=cognito_sub,
)

return {'message': 'OK'}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider refactoring to reduce code duplication.

The handle_privilege_encumbrance_lifting and handle_license_encumbrance_lifting functions share nearly identical logic. Only the final data client method call differs.

Consider extracting the common logic into a shared function:

+def _validate_and_parse_lift_request(event: dict) -> tuple[str, str, str, str, date, str, str]:
+    """Extract and validate common parameters for lifting encumbrances."""
+    # Get the cognito sub of the caller for tracing
+    cognito_sub = _get_submitting_user_id(event)
+    
+    # Extract path parameters
+    compact = event['pathParameters']['compact']
+    provider_id = event['pathParameters']['providerId']
+    jurisdiction = event['pathParameters']['jurisdiction']
+    license_type_abbreviation = event['pathParameters']['licenseType'].lower()
+    
+    # Parse and validate request body
+    body = json.loads(event['body'])
+    effective_lift_date = body['effectiveLiftDate']
+    encumbrance_id = body['encumbranceId']
+    
+    # Validate date format and parse the effective lift date
+    try:
+        lift_date = datetime.fromisoformat(effective_lift_date).date()
+    except ValueError as e:
+        raise CCInvalidRequestException('Invalid date format. Expected ISO format (YYYY-MM-DD)') from e
+    
+    current_date = config.expiration_resolution_date
+    
+    if lift_date > current_date:
+        raise CCInvalidRequestException('The lift date must not be a future date')
+    
+    return compact, provider_id, jurisdiction, license_type_abbreviation, lift_date, encumbrance_id, cognito_sub


 def handle_privilege_encumbrance_lifting(event: dict) -> dict:
     """Handle lifting encumbrance from a privilege record"""
-    # Get the cognito sub of the caller for tracing
-    cognito_sub = _get_submitting_user_id(event)
-
-    with logger.append_context_keys(cognito_sub=cognito_sub):
+    compact, provider_id, jurisdiction, license_type_abbreviation, lift_date, encumbrance_id, cognito_sub = (
+        _validate_and_parse_lift_request(event)
+    )
+    
+    with logger.append_context_keys(cognito_sub=cognito_sub):
         logger.info('Processing privilege encumbrance lifting')
-
-        # Extract path parameters
-        compact = event['pathParameters']['compact']
-        provider_id = event['pathParameters']['providerId']
-        jurisdiction = event['pathParameters']['jurisdiction']
-        license_type_abbreviation = event['pathParameters']['licenseType'].lower()
-
-        # Parse and validate request body
-        body = json.loads(event['body'])
-        effective_lift_date = body['effectiveLiftDate']
-        encumbrance_id = body['encumbranceId']
-
-        # Validate date format and parse the effective lift date
-        try:
-            lift_date = datetime.fromisoformat(effective_lift_date).date()
-        except ValueError as e:
-            raise CCInvalidRequestException('Invalid date format. Expected ISO format (YYYY-MM-DD)') from e
-
-        current_date = config.expiration_resolution_date
-
-        if lift_date > current_date:
-            raise CCInvalidRequestException('The lift date must not be a future date')
-
+        
         # Call the data client method to lift the privilege encumbrance
         config.data_client.lift_privilege_encumbrance(
             compact=compact,
             provider_id=provider_id,
             jurisdiction=jurisdiction,
             license_type_abbreviation=license_type_abbreviation,
             adverse_action_id=encumbrance_id,
             effective_lift_date=lift_date,
             lifting_user=cognito_sub,
         )
-
+        
         return {'message': 'OK'}


 def handle_license_encumbrance_lifting(event: dict) -> dict:
     """Handle lifting encumbrance from a license record"""
-    # Get the cognito sub of the caller for tracing
-    cognito_sub = _get_submitting_user_id(event)
-
-    with logger.append_context_keys(cognito_sub=cognito_sub):
+    compact, provider_id, jurisdiction, license_type_abbreviation, lift_date, encumbrance_id, cognito_sub = (
+        _validate_and_parse_lift_request(event)
+    )
+    
+    with logger.append_context_keys(cognito_sub=cognito_sub):
         logger.info('Processing license encumbrance lifting')
-
-        # Extract path parameters
-        compact = event['pathParameters']['compact']
-        provider_id = event['pathParameters']['providerId']
-        jurisdiction = event['pathParameters']['jurisdiction']
-        license_type_abbreviation = event['pathParameters']['licenseType'].lower()
-
-        # Parse and validate request body
-        body = json.loads(event['body'])
-        effective_lift_date = body['effectiveLiftDate']
-        encumbrance_id = body['encumbranceId']
-
-        # Validate date format and parse the effective lift date
-        try:
-            lift_date = datetime.fromisoformat(effective_lift_date).date()
-        except ValueError as e:
-            raise CCInvalidRequestException('Invalid date format. Expected ISO format (YYYY-MM-DD)') from e
-
-        current_date = config.expiration_resolution_date
-
-        if lift_date > current_date:
-            raise CCInvalidRequestException('The lift date must not be a future date')
-
+        
         # Call the data client method to lift the license encumbrance
         config.data_client.lift_license_encumbrance(
             compact=compact,
             provider_id=provider_id,
             jurisdiction=jurisdiction,
             license_type_abbreviation=license_type_abbreviation,
             adverse_action_id=encumbrance_id,
             effective_lift_date=lift_date,
             lifting_user=cognito_sub,
         )
-
+        
         return {'message': 'OK'}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def handle_privilege_encumbrance_lifting(event: dict) -> dict:
"""Handle lifting encumbrance from a privilege record"""
# Get the cognito sub of the caller for tracing
cognito_sub = _get_submitting_user_id(event)
with logger.append_context_keys(cognito_sub=cognito_sub):
logger.info('Processing privilege encumbrance lifting')
# Extract path parameters
compact = event['pathParameters']['compact']
provider_id = event['pathParameters']['providerId']
jurisdiction = event['pathParameters']['jurisdiction']
license_type_abbreviation = event['pathParameters']['licenseType'].lower()
# Parse and validate request body
body = json.loads(event['body'])
effective_lift_date = body['effectiveLiftDate']
encumbrance_id = body['encumbranceId']
# Validate date format and parse the effective lift date
try:
lift_date = datetime.fromisoformat(effective_lift_date).date()
except ValueError as e:
raise CCInvalidRequestException('Invalid date format. Expected ISO format (YYYY-MM-DD)') from e
current_date = config.expiration_resolution_date
if lift_date > current_date:
raise CCInvalidRequestException('The lift date must not be a future date')
# Call the data client method to lift the privilege encumbrance
config.data_client.lift_privilege_encumbrance(
compact=compact,
provider_id=provider_id,
jurisdiction=jurisdiction,
license_type_abbreviation=license_type_abbreviation,
adverse_action_id=encumbrance_id,
effective_lift_date=lift_date,
lifting_user=cognito_sub,
)
return {'message': 'OK'}
def handle_license_encumbrance_lifting(event: dict) -> dict:
"""Handle lifting encumbrance from a license record"""
# Get the cognito sub of the caller for tracing
cognito_sub = _get_submitting_user_id(event)
with logger.append_context_keys(cognito_sub=cognito_sub):
logger.info('Processing license encumbrance lifting')
# Extract path parameters
compact = event['pathParameters']['compact']
provider_id = event['pathParameters']['providerId']
jurisdiction = event['pathParameters']['jurisdiction']
license_type_abbreviation = event['pathParameters']['licenseType'].lower()
# Parse and validate request body
body = json.loads(event['body'])
effective_lift_date = body['effectiveLiftDate']
encumbrance_id = body['encumbranceId']
# Validate date format and parse the effective lift date
try:
lift_date = datetime.fromisoformat(effective_lift_date).date()
except ValueError as e:
raise CCInvalidRequestException('Invalid date format. Expected ISO format (YYYY-MM-DD)') from e
current_date = config.expiration_resolution_date
if lift_date > current_date:
raise CCInvalidRequestException('The lift date must not be a future date')
# Call the data client method to lift the license encumbrance
config.data_client.lift_license_encumbrance(
compact=compact,
provider_id=provider_id,
jurisdiction=jurisdiction,
license_type_abbreviation=license_type_abbreviation,
adverse_action_id=encumbrance_id,
effective_lift_date=lift_date,
lifting_user=cognito_sub,
)
return {'message': 'OK'}
def _validate_and_parse_lift_request(event: dict) -> tuple[str, str, str, str, date, str, str]:
"""Extract and validate common parameters for lifting encumbrances."""
# Get the cognito sub of the caller for tracing
cognito_sub = _get_submitting_user_id(event)
# Extract path parameters
compact = event['pathParameters']['compact']
provider_id = event['pathParameters']['providerId']
jurisdiction = event['pathParameters']['jurisdiction']
license_type_abbreviation = event['pathParameters']['licenseType'].lower()
# Parse and validate request body
body = json.loads(event['body'])
effective_lift_date = body['effectiveLiftDate']
encumbrance_id = body['encumbranceId']
# Validate date format and parse the effective lift date
try:
lift_date = datetime.fromisoformat(effective_lift_date).date()
except ValueError as e:
raise CCInvalidRequestException('Invalid date format. Expected ISO format (YYYY-MM-DD)') from e
current_date = config.expiration_resolution_date
if lift_date > current_date:
raise CCInvalidRequestException('The lift date must not be a future date')
return compact, provider_id, jurisdiction, license_type_abbreviation, lift_date, encumbrance_id, cognito_sub
def handle_privilege_encumbrance_lifting(event: dict) -> dict:
"""Handle lifting encumbrance from a privilege record"""
compact, provider_id, jurisdiction, license_type_abbreviation, lift_date, encumbrance_id, cognito_sub = (
_validate_and_parse_lift_request(event)
)
with logger.append_context_keys(cognito_sub=cognito_sub):
logger.info('Processing privilege encumbrance lifting')
# Call the data client method to lift the privilege encumbrance
config.data_client.lift_privilege_encumbrance(
compact=compact,
provider_id=provider_id,
jurisdiction=jurisdiction,
license_type_abbreviation=license_type_abbreviation,
adverse_action_id=encumbrance_id,
effective_lift_date=lift_date,
lifting_user=cognito_sub,
)
return {'message': 'OK'}
def handle_license_encumbrance_lifting(event: dict) -> dict:
"""Handle lifting encumbrance from a license record"""
compact, provider_id, jurisdiction, license_type_abbreviation, lift_date, encumbrance_id, cognito_sub = (
_validate_and_parse_lift_request(event)
)
with logger.append_context_keys(cognito_sub=cognito_sub):
logger.info('Processing license encumbrance lifting')
# Call the data client method to lift the license encumbrance
config.data_client.lift_license_encumbrance(
compact=compact,
provider_id=provider_id,
jurisdiction=jurisdiction,
license_type_abbreviation=license_type_abbreviation,
adverse_action_id=encumbrance_id,
effective_lift_date=lift_date,
lifting_user=cognito_sub,
)
return {'message': 'OK'}
🤖 Prompt for AI Agents
In
backend/compact-connect/lambdas/python/provider-data-v1/handlers/encumbrance.py
between lines 116 and 201, the functions handle_privilege_encumbrance_lifting
and handle_license_encumbrance_lifting contain nearly identical code except for
the final data client method called. Refactor by extracting the shared
logic—such as extracting path parameters, parsing and validating the request
body, and date validation—into a single helper function that accepts the
specific data client method as a parameter. Then, update the original functions
to call this helper with the appropriate method, reducing duplication and
improving maintainability.

Comment on lines +258 to +283
def get_privilege_records(
self,
filter_condition: Callable[[PrivilegeData], bool] | None = None,
) -> list[PrivilegeData]:
"""
Get all privilege records from a list of provider records.
:param filter_condition: An optional filter to apply to the privilege records
"""
return [
PrivilegeData.from_database_record(record)
for record in ProviderRecordUtility.get_records_of_type(self.provider_records, ProviderRecordType.PRIVILEGE)
if (filter_condition is None or filter_condition(PrivilegeData.from_database_record(record)))
]

def get_license_records(
self,
filter_condition: Callable[[LicenseData], bool] | None = None,
) -> list[LicenseData]:
"""
Get all license records from a list of provider records.
"""
return [
LicenseData.from_database_record(record)
for record in ProviderRecordUtility.get_records_of_type(self.provider_records, ProviderRecordType.LICENSE)
if (filter_condition is None or filter_condition(LicenseData.from_database_record(record)))
]
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Optimize record filtering to avoid duplicate conversions.

The current implementation converts each record to a data class twice when a filter is applied - once for the filter check and once for the final list. This is inefficient, especially with large record sets.

Apply this optimization to both methods:

 def get_privilege_records(
     self,
     filter_condition: Callable[[PrivilegeData], bool] | None = None,
 ) -> list[PrivilegeData]:
     """
     Get all privilege records from a list of provider records.
     :param filter_condition: An optional filter to apply to the privilege records
     """
-    return [
-        PrivilegeData.from_database_record(record)
-        for record in ProviderRecordUtility.get_records_of_type(self.provider_records, ProviderRecordType.PRIVILEGE)
-        if (filter_condition is None or filter_condition(PrivilegeData.from_database_record(record)))
-    ]
+    privilege_data_list = [
+        PrivilegeData.from_database_record(record)
+        for record in ProviderRecordUtility.get_records_of_type(self.provider_records, ProviderRecordType.PRIVILEGE)
+    ]
+    if filter_condition:
+        return [privilege for privilege in privilege_data_list if filter_condition(privilege)]
+    return privilege_data_list

 def get_license_records(
     self,
     filter_condition: Callable[[LicenseData], bool] | None = None,
 ) -> list[LicenseData]:
     """
     Get all license records from a list of provider records.
     """
-    return [
-        LicenseData.from_database_record(record)
-        for record in ProviderRecordUtility.get_records_of_type(self.provider_records, ProviderRecordType.LICENSE)
-        if (filter_condition is None or filter_condition(LicenseData.from_database_record(record)))
-    ]
+    license_data_list = [
+        LicenseData.from_database_record(record)
+        for record in ProviderRecordUtility.get_records_of_type(self.provider_records, ProviderRecordType.LICENSE)
+    ]
+    if filter_condition:
+        return [license for license in license_data_list if filter_condition(license)]
+    return license_data_list
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_privilege_records(
self,
filter_condition: Callable[[PrivilegeData], bool] | None = None,
) -> list[PrivilegeData]:
"""
Get all privilege records from a list of provider records.
:param filter_condition: An optional filter to apply to the privilege records
"""
return [
PrivilegeData.from_database_record(record)
for record in ProviderRecordUtility.get_records_of_type(self.provider_records, ProviderRecordType.PRIVILEGE)
if (filter_condition is None or filter_condition(PrivilegeData.from_database_record(record)))
]
def get_license_records(
self,
filter_condition: Callable[[LicenseData], bool] | None = None,
) -> list[LicenseData]:
"""
Get all license records from a list of provider records.
"""
return [
LicenseData.from_database_record(record)
for record in ProviderRecordUtility.get_records_of_type(self.provider_records, ProviderRecordType.LICENSE)
if (filter_condition is None or filter_condition(LicenseData.from_database_record(record)))
]
def get_privilege_records(
self,
filter_condition: Callable[[PrivilegeData], bool] | None = None,
) -> list[PrivilegeData]:
"""
Get all privilege records from a list of provider records.
:param filter_condition: An optional filter to apply to the privilege records
"""
privilege_data_list = [
PrivilegeData.from_database_record(record)
for record in ProviderRecordUtility.get_records_of_type(
self.provider_records,
ProviderRecordType.PRIVILEGE
)
]
if filter_condition:
return [priv for priv in privilege_data_list if filter_condition(priv)]
return privilege_data_list
def get_license_records(
self,
filter_condition: Callable[[LicenseData], bool] | None = None,
) -> list[LicenseData]:
"""
Get all license records from a list of provider records.
"""
license_data_list = [
LicenseData.from_database_record(record)
for record in ProviderRecordUtility.get_records_of_type(
self.provider_records,
ProviderRecordType.LICENSE
)
]
if filter_condition:
return [lic for lic in license_data_list if filter_condition(lic)]
return license_data_list
🤖 Prompt for AI Agents
In
backend/compact-connect/lambdas/python/common/cc_common/data_model/provider_record_util.py
around lines 258 to 283, both get_privilege_records and get_license_records
convert each record twice when a filter_condition is provided, causing
inefficiency. To fix this, refactor each method to convert the record once,
store the converted object in a variable, then apply the filter_condition on
that variable before including it in the result list. This avoids duplicate
conversions and improves performance.

Comment on lines +1357 to +1490
provider_user_records = self.get_provider_user_records(
compact=compact,
provider_id=provider_id,
consistent_read=True,
)

# Get adverse action records for this privilege
adverse_action_records = provider_user_records.get_adverse_action_records_for_privilege(
privilege_jurisdiction=jurisdiction,
privilege_license_type_abbreviation=license_type_abbreviation,
)

# Find the specific adverse action record to lift
target_adverse_action: AdverseActionData | None = None
for adverse_action in adverse_action_records:
if str(adverse_action.adverseActionId) == adverse_action_id:
target_adverse_action = adverse_action
break

if target_adverse_action is None:
raise CCNotFoundException('Encumbrance record not found')

# Check if the adverse action has already been lifted
if target_adverse_action.effectiveLiftDate is not None:
raise CCInvalidRequestException('Encumbrance has already been lifted')

# Check if this is the last remaining unlifted adverse action
unlifted_adverse_actions = [
aa
for aa in adverse_action_records
if aa.effectiveLiftDate is None and str(aa.adverseActionId) != adverse_action_id
]

# Get the privilege record
privilege_records = provider_user_records.get_privilege_records(
filter_condition=lambda p: (
p.jurisdiction == jurisdiction
and p.licenseType
== LicenseUtility.get_license_type_by_abbreviation(compact, license_type_abbreviation).name
)
)

if not privilege_records:
message = 'Privilege record not found for adverse action record.'
logger.error(message)
raise CCInternalException(message)

privilege_data = privilege_records[0]

# Build transaction items
transact_items = []

# Always update the adverse action record with lift information
transact_items.append(
self._generate_adverse_action_lift_update_item(
target_adverse_action=target_adverse_action,
effective_lift_date=effective_lift_date,
lifting_user=lifting_user,
)
)

# If this was the last unlifted adverse action, update privilege status and create update record
if not unlifted_adverse_actions:
# Update privilege record to unencumbered status
privilege_update_item = self._generate_set_privilege_encumbered_status_item(
privilege_data=privilege_data,
privilege_encumbered_status=PrivilegeEncumberedStatusEnum.UNENCUMBERED,
)
transact_items.append(privilege_update_item)

# Create privilege update record
privilege_update_record = PrivilegeUpdateData.create_new(
{
'type': 'privilegeUpdate',
'updateType': UpdateCategory.LIFTING_ENCUMBRANCE,
'providerId': provider_id,
'compact': compact,
'jurisdiction': jurisdiction,
'licenseType': privilege_data.licenseType,
'previous': privilege_data.to_dict(),
'updatedValues': {
'encumberedStatus': PrivilegeEncumberedStatusEnum.UNENCUMBERED,
},
}
).serialize_to_database_record()

transact_items.append(self._generate_put_transaction_item(privilege_update_record))

# Check if provider should be set to unencumbered
provider_status_items = self._generate_provider_encumbered_status_transaction_items_if_no_encumbrances(
provider_user_records=provider_user_records,
lifted_record=privilege_data,
)
transact_items.extend(provider_status_items)

# Execute the transaction
self.config.dynamodb_client.transact_write_items(TransactItems=transact_items)

logger.info('Successfully lifted privilege encumbrance')

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for license type lookup.

The method is well-implemented but needs additional error handling:

  1. Line 1429: LicenseUtility.get_license_type_by_abbreviation could raise an exception if the abbreviation is invalid
  2. Lines 1433-1437: Consider if CCInternalException is the right exception type - this could be a data consistency issue

Add error handling for the license type lookup:

             # Get the privilege record
+            try:
+                license_type_name = LicenseUtility.get_license_type_by_abbreviation(
+                    compact, license_type_abbreviation
+                ).name
+            except (KeyError, ValueError) as e:
+                logger.error('Invalid license type abbreviation', error=str(e))
+                raise CCInvalidRequestException(f'Invalid license type abbreviation: {license_type_abbreviation}') from e
+                
             privilege_records = provider_user_records.get_privilege_records(
                 filter_condition=lambda p: (
                     p.jurisdiction == jurisdiction
-                    and p.licenseType
-                    == LicenseUtility.get_license_type_by_abbreviation(compact, license_type_abbreviation).name
+                    and p.licenseType == license_type_name
                 )
             )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def lift_privilege_encumbrance(
self,
compact: str,
provider_id: str,
jurisdiction: str,
license_type_abbreviation: str,
adverse_action_id: str,
effective_lift_date: date,
lifting_user: str,
) -> None:
"""
Lift an encumbrance from a privilege record by updating the adverse action record
and potentially updating the privilege record's encumbered status.
:param str compact: The compact name
:param str provider_id: The provider ID
:param str jurisdiction: The jurisdiction
:param str license_type_abbreviation: The license type abbreviation
:param str adverse_action_id: The adverse action ID to lift
:param date effective_lift_date: The effective date when the encumbrance is lifted
:param str lifting_user: The cognito sub of the user lifting the encumbrance
:raises CCNotFoundException: If the adverse action record is not found
:raises CCInvalidRequestException: If the encumbrance has already been lifted
"""
with logger.append_context_keys(
compact=compact,
provider_id=provider_id,
jurisdiction=jurisdiction,
license_type_abbreviation=license_type_abbreviation,
adverse_action_id=adverse_action_id,
):
logger.info('Lifting privilege encumbrance')
# Get all provider records
provider_user_records = self.get_provider_user_records(
compact=compact,
provider_id=provider_id,
consistent_read=True,
)
# Get adverse action records for this privilege
adverse_action_records = provider_user_records.get_adverse_action_records_for_privilege(
privilege_jurisdiction=jurisdiction,
privilege_license_type_abbreviation=license_type_abbreviation,
)
# Find the specific adverse action record to lift
target_adverse_action: AdverseActionData | None = None
for adverse_action in adverse_action_records:
if str(adverse_action.adverseActionId) == adverse_action_id:
target_adverse_action = adverse_action
break
if target_adverse_action is None:
raise CCNotFoundException('Encumbrance record not found')
# Check if the adverse action has already been lifted
if target_adverse_action.effectiveLiftDate is not None:
raise CCInvalidRequestException('Encumbrance has already been lifted')
# Check if this is the last remaining unlifted adverse action
unlifted_adverse_actions = [
aa
for aa in adverse_action_records
if aa.effectiveLiftDate is None and str(aa.adverseActionId) != adverse_action_id
]
# Get the privilege record
privilege_records = provider_user_records.get_privilege_records(
filter_condition=lambda p: (
p.jurisdiction == jurisdiction
and p.licenseType
== LicenseUtility.get_license_type_by_abbreviation(compact, license_type_abbreviation).name
)
)
if not privilege_records:
message = 'Privilege record not found for adverse action record.'
logger.error(message)
raise CCInternalException(message)
privilege_data = privilege_records[0]
# Build transaction items
transact_items = []
# Always update the adverse action record with lift information
transact_items.append(
self._generate_adverse_action_lift_update_item(
target_adverse_action=target_adverse_action,
effective_lift_date=effective_lift_date,
lifting_user=lifting_user,
)
)
# If this was the last unlifted adverse action, update privilege status and create update record
if not unlifted_adverse_actions:
# Update privilege record to unencumbered status
privilege_update_item = self._generate_set_privilege_encumbered_status_item(
privilege_data=privilege_data,
privilege_encumbered_status=PrivilegeEncumberedStatusEnum.UNENCUMBERED,
)
transact_items.append(privilege_update_item)
# Create privilege update record
privilege_update_record = PrivilegeUpdateData.create_new(
{
'type': 'privilegeUpdate',
'updateType': UpdateCategory.LIFTING_ENCUMBRANCE,
'providerId': provider_id,
'compact': compact,
'jurisdiction': jurisdiction,
'licenseType': privilege_data.licenseType,
'previous': privilege_data.to_dict(),
'updatedValues': {
'encumberedStatus': PrivilegeEncumberedStatusEnum.UNENCUMBERED,
},
}
).serialize_to_database_record()
transact_items.append(self._generate_put_transaction_item(privilege_update_record))
# Check if provider should be set to unencumbered
provider_status_items = self._generate_provider_encumbered_status_transaction_items_if_no_encumbrances(
provider_user_records=provider_user_records,
lifted_record=privilege_data,
)
transact_items.extend(provider_status_items)
# Execute the transaction
self.config.dynamodb_client.transact_write_items(TransactItems=transact_items)
logger.info('Successfully lifted privilege encumbrance')
# Get the privilege record
try:
license_type_name = LicenseUtility.get_license_type_by_abbreviation(
compact, license_type_abbreviation
).name
except (KeyError, ValueError) as e:
logger.error('Invalid license type abbreviation', error=str(e))
raise CCInvalidRequestException(
f'Invalid license type abbreviation: {license_type_abbreviation}'
) from e
privilege_records = provider_user_records.get_privilege_records(
filter_condition=lambda p: (
p.jurisdiction == jurisdiction
and p.licenseType == license_type_name
)
)
🤖 Prompt for AI Agents
In
backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py
around lines 1429 to 1437, add error handling for the call to
LicenseUtility.get_license_type_by_abbreviation to catch exceptions if the
license type abbreviation is invalid. Wrap this call in a try-except block and
raise a more specific exception or handle the error gracefully. Also, review the
use of CCInternalException when no privilege records are found and consider
raising a different exception that better reflects a data consistency or
validation issue.

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.

Adverse Action - State admins can lift an encumbrance
1 participant