From 4f6e9623becbee229242742b857c9a9dce6e0413 Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Wed, 19 Nov 2025 09:27:13 -0700 Subject: [PATCH 01/14] Add client signature example data --- .../bin/compile_requirements.sh | 7 +- backend/compact-connect/bin/sync_deps.sh | 6 +- .../docs/client_signature_auth.md | 5 +- .../cognito-backup/requirements-dev.txt | 16 +- .../python/cognito-backup/requirements.txt | 6 +- .../python/common/common_test/sign_request.py | 20 +- .../python/common/requirements-dev.txt | 41 ++-- .../lambdas/python/common/requirements.txt | 8 +- .../resources/signature_auth_examples.txt | 145 ++++++++++++++ .../unit/test_signature_auth_integration.py | 180 ++++++++++++++++++ .../requirements-dev.txt | 10 +- .../custom-resources/requirements-dev.txt | 10 +- .../python/data-events/requirements-dev.txt | 10 +- .../disaster-recovery/requirements-dev.txt | 10 +- .../provider-data-v1/requirements-dev.txt | 15 +- .../python/purchases/requirements-dev.txt | 47 ++--- .../lambdas/python/purchases/requirements.txt | 8 +- .../staff-user-pre-token/requirements-dev.txt | 10 +- .../python/staff-users/requirements-dev.txt | 17 +- backend/compact-connect/requirements-dev.txt | 22 +-- backend/compact-connect/requirements.txt | 12 +- 21 files changed, 473 insertions(+), 132 deletions(-) create mode 100644 backend/compact-connect/lambdas/python/common/tests/resources/signature_auth_examples.txt diff --git a/backend/compact-connect/bin/compile_requirements.sh b/backend/compact-connect/bin/compile_requirements.sh index 2a818994b..0706a9062 100755 --- a/backend/compact-connect/bin/compile_requirements.sh +++ b/backend/compact-connect/bin/compile_requirements.sh @@ -16,8 +16,11 @@ pip-compile --no-emit-index-url --upgrade --no-strip-extras lambdas/python/disas pip-compile --no-emit-index-url --upgrade --no-strip-extras lambdas/python/disaster-recovery/requirements.in pip-compile --no-emit-index-url --upgrade --no-strip-extras lambdas/python/provider-data-v1/requirements-dev.in pip-compile --no-emit-index-url --upgrade --no-strip-extras lambdas/python/provider-data-v1/requirements.in -pip-compile --no-emit-index-url --upgrade --no-strip-extras lambdas/python/purchases/requirements-dev.in -pip-compile --no-emit-index-url --upgrade --no-strip-extras lambdas/python/purchases/requirements.in +# The purchases lambda requires Python<=3.12, which is older than everything else in this project, so we have +# to install that separately, if we want to be developing with Python>=3.13 for the rest of the project, to +# avoid installation failures +# pip-compile --no-emit-index-url --upgrade --no-strip-extras lambdas/python/purchases/requirements-dev.in +# pip-compile --no-emit-index-url --upgrade --no-strip-extras lambdas/python/purchases/requirements.in pip-compile --no-emit-index-url --upgrade --no-strip-extras lambdas/python/staff-user-pre-token/requirements-dev.in pip-compile --no-emit-index-url --upgrade --no-strip-extras lambdas/python/staff-user-pre-token/requirements.in pip-compile --no-emit-index-url --upgrade --no-strip-extras lambdas/python/staff-users/requirements-dev.in diff --git a/backend/compact-connect/bin/sync_deps.sh b/backend/compact-connect/bin/sync_deps.sh index 46835a8cd..19e960e08 100755 --- a/backend/compact-connect/bin/sync_deps.sh +++ b/backend/compact-connect/bin/sync_deps.sh @@ -20,8 +20,10 @@ pip-sync \ lambdas/python/disaster-recovery/requirements.txt \ lambdas/python/provider-data-v1/requirements-dev.txt \ lambdas/python/provider-data-v1/requirements.txt \ - lambdas/python/purchases/requirements-dev.txt \ - lambdas/python/purchases/requirements.txt \ +# We have to manage the purchases lambda Python environment separately +# because it is held back to an older version than the rest of the project +# lambdas/python/purchases/requirements-dev.txt \ +# lambdas/python/purchases/requirements.txt \ lambdas/python/staff-user-pre-token/requirements-dev.txt \ lambdas/python/staff-user-pre-token/requirements.txt \ lambdas/python/staff-users/requirements-dev.txt \ diff --git a/backend/compact-connect/docs/client_signature_auth.md b/backend/compact-connect/docs/client_signature_auth.md index ac1a7545a..bf47096dd 100644 --- a/backend/compact-connect/docs/client_signature_auth.md +++ b/backend/compact-connect/docs/client_signature_auth.md @@ -142,8 +142,9 @@ for *both required and optional signature auth endpoints*). #### Python Example We maintain an example implementation, which we use to test and validate our own authentication mechanism -[here](../lambdas/python/common/common_test/sign_request.py). You can use this as a reference for your own -implementation. +[here](../lambdas/python/common/common_test/sign_request.py) and some example HTTP request data in a text file +[here](../lambdas/python/common/tests/resources/signature_auth_examples.txt). You can use this as a reference for your +own implementation. ### Key Management diff --git a/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt b/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt index ccf9009a7..5d4d9e65d 100644 --- a/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt @@ -4,19 +4,19 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/cognito-backup/requirements-dev.in # -aws-lambda-powertools==3.22.0 +aws-lambda-powertools==3.23.0 # via -r lambdas/python/cognito-backup/requirements-dev.in -boto3==1.40.56 +boto3==1.40.76 # via # -r lambdas/python/cognito-backup/requirements-dev.in # moto -botocore==1.40.56 +botocore==1.40.76 # via # -r lambdas/python/cognito-backup/requirements-dev.in # boto3 # moto # s3transfer -certifi==2025.10.5 +certifi==2025.11.12 # via requests cffi==2.0.0 # via cryptography @@ -37,25 +37,25 @@ jmespath==1.0.1 # aws-lambda-powertools # boto3 # botocore -joserfc==1.4.0 +joserfc==1.4.3 # via moto markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[cognitoidp,s3]==5.1.15 +moto[cognitoidp,s3]==5.1.17 # via -r lambdas/python/cognito-backup/requirements-dev.in packaging==25.0 # via pytest pluggy==1.6.0 # via pytest -py-partiql-parser==0.6.1 +py-partiql-parser==0.6.3 # via moto pycparser==2.23 # via cffi pygments==2.19.2 # via pytest -pytest==8.4.2 +pytest==9.0.1 # via -r lambdas/python/cognito-backup/requirements-dev.in python-dateutil==2.9.0.post0 # via diff --git a/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt b/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt index a9ee3d599..44a833959 100644 --- a/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt +++ b/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt @@ -4,11 +4,11 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/cognito-backup/requirements.in # -aws-lambda-powertools==3.22.0 +aws-lambda-powertools==3.23.0 # via -r lambdas/python/cognito-backup/requirements.in -boto3==1.40.56 +boto3==1.40.76 # via -r lambdas/python/cognito-backup/requirements.in -botocore==1.40.56 +botocore==1.40.76 # via # -r lambdas/python/cognito-backup/requirements.in # boto3 diff --git a/backend/compact-connect/lambdas/python/common/common_test/sign_request.py b/backend/compact-connect/lambdas/python/common/common_test/sign_request.py index d456bad83..759237942 100644 --- a/backend/compact-connect/lambdas/python/common/common_test/sign_request.py +++ b/backend/compact-connect/lambdas/python/common/common_test/sign_request.py @@ -46,13 +46,8 @@ def sign_request( :param private_key_pem: PEM-encoded ECDSA private key :return: Dictionary containing headers to add to request """ - # Sort and URL-encode query parameters - sorted_params = '&'.join( - f'{quote(str(k), safe="")}={quote(str(v), safe="")}' for k, v in sorted(query_params.items()) - ) - # Create signature string - signature_string = '\n'.join([method, path, sorted_params, timestamp, nonce, key_id]) + signature_string = get_string_to_sign(method, path, query_params, timestamp, nonce, key_id) # Load private key private_key = serialization.load_pem_private_key(private_key_pem.encode(), password=None) @@ -68,3 +63,16 @@ def sign_request( 'X-Key-Id': key_id, 'X-Signature': base64.b64encode(signature).decode(), } + + +def get_string_to_sign(method: str, path: str, query_params: dict, timestamp: str, nonce: str, key_id: str) -> str: + """ + Get the string to sign for a request. + """ + # Sort and URL-encode query parameters + sorted_params = '&'.join( + f'{quote(str(k), safe="")}={quote(str(v), safe="")}' for k, v in sorted(query_params.items()) + ) + + # Create signature string + return '\n'.join([method, path, sorted_params, timestamp, nonce, key_id]) diff --git a/backend/compact-connect/lambdas/python/common/requirements-dev.txt b/backend/compact-connect/lambdas/python/common/requirements-dev.txt index 65f18cb4d..b89985fed 100644 --- a/backend/compact-connect/lambdas/python/common/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/common/requirements-dev.txt @@ -14,29 +14,29 @@ attrs==25.4.0 # referencing aws-sam-translator==1.101.0 # via cfn-lint -aws-xray-sdk==2.14.0 +aws-xray-sdk==2.15.0 # via moto -boto3==1.40.56 +boto3==1.40.76 # via # aws-sam-translator # moto -boto3-stubs[full]==1.40.56 +boto3-stubs[full]==1.40.76 # via -r lambdas/python/common/requirements-dev.in -boto3-stubs-full==1.40.56 +boto3-stubs-full==1.40.76 # via boto3-stubs -botocore==1.40.56 +botocore==1.40.76 # via # aws-xray-sdk # boto3 # moto # s3transfer -botocore-stubs==1.40.56 +botocore-stubs==1.40.76 # via boto3-stubs -certifi==2025.10.5 +certifi==2025.11.12 # via requests cffi==2.0.0 # via cryptography -cfn-lint==1.40.2 +cfn-lint==1.41.0 # via moto charset-normalizer==3.4.4 # via requests @@ -47,9 +47,9 @@ cryptography==46.0.3 # moto docker==7.1.0 # via moto -faker==28.4.1 +faker==37.12.0 # via -r lambdas/python/common/requirements-dev.in -graphql-core==3.2.6 +graphql-core==3.2.7 # via moto idna==3.11 # via requests @@ -59,7 +59,7 @@ jmespath==1.0.1 # via # boto3 # botocore -joserfc==1.4.0 +joserfc==1.4.3 # via moto jsonpatch==1.33 # via cfn-lint @@ -85,7 +85,7 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[all]==5.1.15 +moto[all]==5.1.17 # via -r lambdas/python/common/requirements-dev.in mpmath==1.3.0 # via sympy @@ -101,20 +101,19 @@ pathable==0.4.4 # via jsonschema-path ply==3.11 # via jsonpath-ng -py-partiql-parser==0.6.1 +py-partiql-parser==0.6.3 # via moto pycparser==2.23 # via cffi -pydantic==2.12.3 +pydantic==2.12.4 # via aws-sam-translator -pydantic-core==2.41.4 +pydantic-core==2.41.5 # via pydantic pyparsing==3.2.5 # via moto python-dateutil==2.9.0.post0 # via # botocore - # faker # moto pyyaml==6.0.3 # via @@ -127,7 +126,7 @@ referencing==0.36.2 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2025.10.23 +regex==2025.11.3 # via cfn-lint requests==2.32.5 # via @@ -139,7 +138,7 @@ responses==0.25.8 # via moto rfc3339-validator==0.1.4 # via openapi-schema-validator -rpds-py==0.27.1 +rpds-py==0.29.0 # via # jsonschema # referencing @@ -151,7 +150,7 @@ six==1.17.0 # rfc3339-validator sympy==1.14.0 # via cfn-lint -types-awscrt==0.28.2 +types-awscrt==0.28.4 # via botocore-stubs types-s3transfer==0.14.0 # via boto3-stubs @@ -164,6 +163,8 @@ typing-extensions==4.15.0 # typing-inspection typing-inspection==0.4.2 # via pydantic +tzdata==2025.2 + # via faker urllib3==2.5.0 # via # botocore @@ -172,7 +173,7 @@ urllib3==2.5.0 # responses werkzeug==3.1.3 # via moto -wrapt==2.0.0 +wrapt==2.0.1 # via aws-xray-sdk xmltodict==1.0.2 # via moto diff --git a/backend/compact-connect/lambdas/python/common/requirements.txt b/backend/compact-connect/lambdas/python/common/requirements.txt index ce0d0029d..b74f5a66a 100644 --- a/backend/compact-connect/lambdas/python/common/requirements.txt +++ b/backend/compact-connect/lambdas/python/common/requirements.txt @@ -8,15 +8,15 @@ argon2-cffi==25.1.0 # via -r lambdas/python/common/requirements.in argon2-cffi-bindings==25.1.0 # via argon2-cffi -aws-lambda-powertools==3.22.0 +aws-lambda-powertools==3.23.0 # via -r lambdas/python/common/requirements.in -boto3==1.40.56 +boto3==1.40.76 # via -r lambdas/python/common/requirements.in -botocore==1.40.56 +botocore==1.40.76 # via # boto3 # s3transfer -certifi==2025.10.5 +certifi==2025.11.12 # via requests cffi==2.0.0 # via diff --git a/backend/compact-connect/lambdas/python/common/tests/resources/signature_auth_examples.txt b/backend/compact-connect/lambdas/python/common/tests/resources/signature_auth_examples.txt new file mode 100644 index 000000000..06419ba48 --- /dev/null +++ b/backend/compact-connect/lambdas/python/common/tests/resources/signature_auth_examples.txt @@ -0,0 +1,145 @@ +================================================================================ +Signature Authentication Examples +================================================================================ + +This document provides example HTTP requests demonstrating the +CompactConnect signature authentication scheme. + +Each example includes: + 1. The raw HTTP request with signature headers + 2. The plaintext string that was signed + 3. The base64-encoded string that was signed + +================================================================================ + +Example 1: GET /v1/compacts/aslp/jurisdictions/al/providers/query +-------------------------------------------------------------------------------- + +Raw HTTP Request: + +GET /v1/compacts/aslp/jurisdictions/al/providers/query?limit=10&offset=0&status=active HTTP/1.1 +Host: api.example.com +Content-Type: application/json +User-Agent: CompactConnect-Client/1.0 +X-Algorithm: ECDSA-SHA256 +X-Timestamp: 2025-11-18T20:39:47.299159Z +X-Nonce: 753e75a390774b7faaa8831a77eb6be4 +X-Key-Id: test-key-001 +X-Signature: MEUCIQDJB+rNp/0vr5x/aXbBIppyi6lHdUMFAfkk6AIQpD277AIgTm2X/CAcFJyO4gU98x0KqfVjwIG9Q1WNVWTs1HDiwZA= + + +Plaintext String to Sign: + +GET +/v1/compacts/aslp/jurisdictions/al/providers/query +limit=10&offset=0&status=active +2025-11-18T20:39:47.299159Z +753e75a390774b7faaa8831a77eb6be4 +test-key-001 + + +Base64-Encoded String to Sign: + +R0VUCi92MS9jb21wYWN0cy9hc2xwL2p1cmlzZGljdGlvbnMvYWwvcHJvdmlkZXJzL3F1ZXJ5CmxpbWl0PTEwJm9mZnNldD0wJnN0YXR1cz1hY3RpdmUKMjAyNS0xMS0xOFQyMDozOTo0Ny4yOTkxNTlaCjc1M2U3NWEzOTA3NzRiN2ZhYWE4ODMxYTc3ZWI2YmU0CnRlc3Qta2V5LTAwMQ== + + +================================================================================ + +Example 2: POST /v1/compacts/aslp/jurisdictions/al/providers +-------------------------------------------------------------------------------- + +Raw HTTP Request: + +POST /v1/compacts/aslp/jurisdictions/al/providers?validate=true HTTP/1.1 +Host: api.example.com +Content-Type: application/json +User-Agent: CompactConnect-Client/1.0 +X-Algorithm: ECDSA-SHA256 +X-Timestamp: 2025-11-18T20:39:47.308195Z +X-Nonce: 7da26c6cb66a4fd2ad137afb4c5e8c4a +X-Key-Id: test-key-002 +X-Signature: MEQCIDI6WiwTS6ZNYEsHRUFCTy8X/Sa5JpfArVM4TOvFujGnAiA1a4Ma6MCU2NweFVsNnB4ZWXUr7NQRdohVPY4feKsMJA== + + +Plaintext String to Sign: + +POST +/v1/compacts/aslp/jurisdictions/al/providers +validate=true +2025-11-18T20:39:47.308195Z +7da26c6cb66a4fd2ad137afb4c5e8c4a +test-key-002 + + +Base64-Encoded String to Sign: + +UE9TVAovdjEvY29tcGFjdHMvYXNscC9qdXJpc2RpY3Rpb25zL2FsL3Byb3ZpZGVycwp2YWxpZGF0ZT10cnVlCjIwMjUtMTEtMThUMjA6Mzk6NDcuMzA4MTk1Wgo3ZGEyNmM2Y2I2NmE0ZmQyYWQxMzdhZmI0YzVlOGM0YQp0ZXN0LWtleS0wMDI= + + +================================================================================ + +Example 3: GET /v1/compacts/aslp/jurisdictions/al/providers/12345 +-------------------------------------------------------------------------------- + +Raw HTTP Request: + +GET /v1/compacts/aslp/jurisdictions/al/providers/12345 HTTP/1.1 +Host: api.example.com +Content-Type: application/json +User-Agent: CompactConnect-Client/1.0 +X-Algorithm: ECDSA-SHA256 +X-Timestamp: 2025-11-18T20:39:47.308296Z +X-Nonce: bc4340855ef1483aa69bcf91368becbe +X-Key-Id: test-key-003 +X-Signature: MEUCIQDOm302tChMchtofb1n9bVNpCPaia/VCWWPrK16Dip8MwIgXH/M+HFoqv69+wo84jmHZnAw1B4IoFU6RQ+B+v9ZS18= + + +Plaintext String to Sign: + +GET +/v1/compacts/aslp/jurisdictions/al/providers/12345 + +2025-11-18T20:39:47.308296Z +bc4340855ef1483aa69bcf91368becbe +test-key-003 + + +Base64-Encoded String to Sign: + +R0VUCi92MS9jb21wYWN0cy9hc2xwL2p1cmlzZGljdGlvbnMvYWwvcHJvdmlkZXJzLzEyMzQ1CgoyMDI1LTExLTE4VDIwOjM5OjQ3LjMwODI5NloKYmM0MzQwODU1ZWYxNDgzYWE2OWJjZjkxMzY4YmVjYmUKdGVzdC1rZXktMDAz + + +================================================================================ + +Example 4: POST /path +-------------------------------------------------------------------------------- + +Raw HTTP Request: + +POST /path?a=1&b=value%20two HTTP/1.1 +Host: api.example.com +Content-Type: application/json +User-Agent: CompactConnect-Client/1.0 +X-Algorithm: ECDSA-SHA256 +X-Timestamp: 2025-11-11T19:09:53Z +X-Nonce: 54ebdc56-4eae-4627-94e1-11ff27a3ec88 +X-Key-Id: eLicenseKey +X-Signature: MEYCIQCXzKEPheBUp2UeDGIY/aN4KDjW0TcedI7QnJBqcjNcXAIhAPSMlZF82qBwzE1trHvkdRvrHPFfqxfnfrlrqpMOCXKU + + +Plaintext String to Sign: + +POST +/path +a=1&b=value%20two +2025-11-11T19:09:53Z +54ebdc56-4eae-4627-94e1-11ff27a3ec88 +eLicenseKey + + +Base64-Encoded String to Sign: + +UE9TVAovcGF0aAphPTEmYj12YWx1ZSUyMHR3bwoyMDI1LTExLTExVDE5OjA5OjUzWgo1NGViZGM1Ni00ZWFlLTQ2MjctOTRlMS0xMWZmMjdhM2VjODgKZUxpY2Vuc2VLZXk= + + +================================================================================ diff --git a/backend/compact-connect/lambdas/python/common/tests/unit/test_signature_auth_integration.py b/backend/compact-connect/lambdas/python/common/tests/unit/test_signature_auth_integration.py index 2eb03a27b..7b14fa90c 100644 --- a/backend/compact-connect/lambdas/python/common/tests/unit/test_signature_auth_integration.py +++ b/backend/compact-connect/lambdas/python/common/tests/unit/test_signature_auth_integration.py @@ -3,6 +3,7 @@ from copy import deepcopy from datetime import UTC, datetime from unittest.mock import patch +from uuid import uuid4 from aws_lambda_powertools.utilities.typing import LambdaContext @@ -234,3 +235,182 @@ def _create_signed_event(self) -> dict: event['headers'].update(headers) return event + + +class TestSignatureAuthSigner(TstLambdas): + def setUp(self): + """Set up test fixtures.""" + super().setUp() + + # Load test keys + with open('tests/resources/client_private_key.pem') as f: + self.private_key_pem = f.read() + + def test_string_to_sign(self): + from common_test.sign_request import get_string_to_sign + # Generate current timestamp and nonce + timestamp = '2025-11-11T19:09:53Z' + nonce = '54ebdc56-4eae-4627-94e1-11ff27a3ec88' + + string_to_sign = get_string_to_sign( + method='POST', + path='/path', + query_params={ + 'a': '1', + 'b': 'value two', + }, + timestamp=timestamp, + nonce=nonce, + key_id='eLicenseKey' + ) + expected = ( + 'POST\n/path\na=1&b=value%20two\n2025-11-11T19:09:53Z\n' + '54ebdc56-4eae-4627-94e1-11ff27a3ec88\neLicenseKey' + ) + self.assertEqual(string_to_sign, expected) + + def test_generate_signature_examples(self): + """Generate example HTTP requests with signature authentication for client documentation.""" + import base64 + from datetime import UTC, datetime + from urllib.parse import quote + + from common_test.sign_request import get_string_to_sign, sign_request + + # Define four example requests with varying methods, paths, and query parameters + examples = [ + { + 'method': 'GET', + 'path': '/v1/compacts/aslp/jurisdictions/al/providers/query', + 'query_params': {'limit': '10', 'offset': '0', 'status': 'active'}, + 'host': 'api.example.com', + 'key_id': 'test-key-001', + }, + { + 'method': 'POST', + 'path': '/v1/compacts/aslp/jurisdictions/al/providers', + 'query_params': {'validate': 'true'}, + 'host': 'api.example.com', + 'key_id': 'test-key-002', + }, + { + 'method': 'GET', + 'path': '/v1/compacts/aslp/jurisdictions/al/providers/12345', + 'query_params': {}, + 'host': 'api.example.com', + 'key_id': 'test-key-003', + }, + { + 'method': 'POST', + 'path': '/path', + 'query_params': {'a': '1', 'b': 'value two'}, + 'host': 'api.example.com', + 'key_id': 'eLicenseKey', + 'timestamp': '2025-11-11T19:09:53Z', + 'nonce': '54ebdc56-4eae-4627-94e1-11ff27a3ec88', + }, + ] + + output_lines = [] + output_lines.append('=' * 80) + output_lines.append('Signature Authentication Examples') + output_lines.append('=' * 80) + output_lines.append('') + output_lines.append('This document provides example HTTP requests demonstrating the') + output_lines.append('CompactConnect signature authentication scheme.') + output_lines.append('') + output_lines.append('Each example includes:') + output_lines.append(' 1. The raw HTTP request with signature headers') + output_lines.append(' 2. The plaintext string that was signed') + output_lines.append(' 3. The base64-encoded string that was signed') + output_lines.append('') + output_lines.append('=' * 80) + output_lines.append('') + + for idx, example in enumerate(examples, 1): + # Generate timestamp and nonce for this example (use provided values if available) + timestamp = example.get('timestamp') or datetime.now(UTC).isoformat().replace('+00:00', 'Z') + nonce = example.get('nonce') or uuid4().hex + + # Get the string to sign + string_to_sign = get_string_to_sign( + method=example['method'], + path=example['path'], + query_params=example['query_params'], + timestamp=timestamp, + nonce=nonce, + key_id=example['key_id'], + ) + + # Sign the request + signature_headers = sign_request( + method=example['method'], + path=example['path'], + query_params=example['query_params'], + timestamp=timestamp, + nonce=nonce, + key_id=example['key_id'], + private_key_pem=self.private_key_pem, + ) + + # Build the query string for the HTTP request (using same format as signature) + if example['query_params']: + sorted_params = '&'.join( + f'{quote(str(k), safe="")}={quote(str(v), safe="")}' + for k, v in sorted(example['query_params'].items()) + ) + query_string = '?' + sorted_params + else: + query_string = '' + + # Format the raw HTTP request + output_lines.append(f'Example {idx}: {example["method"]} {example["path"]}') + output_lines.append('-' * 80) + output_lines.append('') + output_lines.append('Raw HTTP Request:') + output_lines.append('') + output_lines.append(f'{example["method"]} {example["path"]}{query_string} HTTP/1.1') + output_lines.append(f'Host: {example["host"]}') + output_lines.append('Content-Type: application/json') + output_lines.append('User-Agent: CompactConnect-Client/1.0') + output_lines.append(f'X-Algorithm: {signature_headers["X-Algorithm"]}') + output_lines.append(f'X-Timestamp: {signature_headers["X-Timestamp"]}') + output_lines.append(f'X-Nonce: {signature_headers["X-Nonce"]}') + output_lines.append(f'X-Key-Id: {signature_headers["X-Key-Id"]}') + output_lines.append(f'X-Signature: {signature_headers["X-Signature"]}') + output_lines.append('') + output_lines.append('') + + # Add the plaintext string to sign + output_lines.append('Plaintext String to Sign:') + output_lines.append('') + output_lines.append(string_to_sign) + output_lines.append('') + output_lines.append('') + + # Add the base64-encoded string to sign + string_to_sign_b64 = base64.b64encode(string_to_sign.encode()).decode() + output_lines.append('Base64-Encoded String to Sign:') + output_lines.append('') + output_lines.append(string_to_sign_b64) + output_lines.append('') + output_lines.append('') + output_lines.append('=' * 80) + output_lines.append('') + + # Write to file + output_file = 'tests/resources/signature_auth_examples.txt' + with open(output_file, 'w') as f: + f.write('\n'.join(output_lines)) + + # Verify the file was created and has content + with open(output_file) as f: + content = f.read() + self.assertGreater(len(content), 0) + self.assertIn('Example 1:', content) + self.assertIn('Example 2:', content) + self.assertIn('Example 3:', content) + self.assertIn('Example 4:', content) + self.assertIn('Raw HTTP Request:', content) + self.assertIn('Plaintext String to Sign:', content) + self.assertIn('Base64-Encoded String to Sign:', content) diff --git a/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt b/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt index c741541bc..326b79307 100644 --- a/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt @@ -4,14 +4,14 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/compact-configuration/requirements-dev.in # -boto3==1.40.56 +boto3==1.40.76 # via moto -botocore==1.40.56 +botocore==1.40.76 # via # boto3 # moto # s3transfer -certifi==2025.10.5 +certifi==2025.11.12 # via requests cffi==2.0.0 # via cryptography @@ -33,9 +33,9 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb,s3]==5.1.15 +moto[dynamodb,s3]==5.1.17 # via -r lambdas/python/compact-configuration/requirements-dev.in -py-partiql-parser==0.6.1 +py-partiql-parser==0.6.3 # via moto pycparser==2.23 # via cffi diff --git a/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt b/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt index 0ac54cfb6..5466f25c7 100644 --- a/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt @@ -4,14 +4,14 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/custom-resources/requirements-dev.in # -boto3==1.40.56 +boto3==1.40.76 # via moto -botocore==1.40.56 +botocore==1.40.76 # via # boto3 # moto # s3transfer -certifi==2025.10.5 +certifi==2025.11.12 # via requests cffi==2.0.0 # via cryptography @@ -33,9 +33,9 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb,s3]==5.1.15 +moto[dynamodb,s3]==5.1.17 # via -r lambdas/python/custom-resources/requirements-dev.in -py-partiql-parser==0.6.1 +py-partiql-parser==0.6.3 # via moto pycparser==2.23 # via cffi diff --git a/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt b/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt index e081f34c9..039e3009d 100644 --- a/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt @@ -4,14 +4,14 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/data-events/requirements-dev.in # -boto3==1.40.56 +boto3==1.40.76 # via moto -botocore==1.40.56 +botocore==1.40.76 # via # boto3 # moto # s3transfer -certifi==2025.10.5 +certifi==2025.11.12 # via requests cffi==2.0.0 # via cryptography @@ -33,9 +33,9 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb,s3]==5.1.15 +moto[dynamodb,s3]==5.1.17 # via -r lambdas/python/data-events/requirements-dev.in -py-partiql-parser==0.6.1 +py-partiql-parser==0.6.3 # via moto pycparser==2.23 # via cffi diff --git a/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt b/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt index 050970754..002774c92 100644 --- a/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt @@ -4,14 +4,14 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/disaster-recovery/requirements-dev.in # -boto3==1.40.56 +boto3==1.40.76 # via moto -botocore==1.40.56 +botocore==1.40.76 # via # boto3 # moto # s3transfer -certifi==2025.10.5 +certifi==2025.11.12 # via requests cffi==2.0.0 # via cryptography @@ -33,9 +33,9 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb,s3]==5.1.15 +moto[dynamodb,s3]==5.1.17 # via -r lambdas/python/disaster-recovery/requirements-dev.in -py-partiql-parser==0.6.1 +py-partiql-parser==0.6.3 # via moto pycparser==2.23 # via cffi diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt b/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt index 62531f73c..ac016de3c 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt @@ -4,14 +4,14 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/provider-data-v1/requirements-dev.in # -boto3==1.40.56 +boto3==1.40.76 # via moto -botocore==1.40.56 +botocore==1.40.76 # via # boto3 # moto # s3transfer -certifi==2025.10.5 +certifi==2025.11.12 # via requests cffi==2.0.0 # via cryptography @@ -21,7 +21,7 @@ cryptography==46.0.3 # via moto docker==7.1.0 # via moto -faker==28.4.1 +faker==37.12.0 # via -r lambdas/python/provider-data-v1/requirements-dev.in idna==3.11 # via requests @@ -35,16 +35,15 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb,s3]==5.1.15 +moto[dynamodb,s3]==5.1.17 # via -r lambdas/python/provider-data-v1/requirements-dev.in -py-partiql-parser==0.6.1 +py-partiql-parser==0.6.3 # via moto pycparser==2.23 # via cffi python-dateutil==2.9.0.post0 # via # botocore - # faker # moto pyyaml==6.0.3 # via @@ -61,6 +60,8 @@ s3transfer==0.14.0 # via boto3 six==1.17.0 # via python-dateutil +tzdata==2025.2 + # via faker urllib3==2.5.0 # via # botocore diff --git a/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt b/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt index 0468753c3..ef723afd3 100644 --- a/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt @@ -1,35 +1,35 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --no-emit-index-url --no-strip-extras lambdas/python/purchases/requirements-dev.in +# pip-compile requirements-dev.in # boolean-py==5.0 # via license-expression -boto3==1.40.56 +boto3==1.40.76 # via moto -botocore==1.40.56 +botocore==1.40.76 # via # boto3 # moto # s3transfer build==1.3.0 # via pip-tools -cachecontrol[filecache]==0.14.3 +cachecontrol[filecache]==0.14.4 # via # cachecontrol # pip-audit -certifi==2025.10.5 +certifi==2025.11.12 # via requests cffi==2.0.0 # via cryptography charset-normalizer==3.4.4 # via requests -click==8.3.0 +click==8.3.1 # via pip-tools -coverage[toml]==7.11.0 +coverage[toml]==7.12.0 # via - # -r lambdas/python/purchases/requirements-dev.in + # -r requirements-dev.in # pytest-cov cryptography==46.0.3 # via moto @@ -39,8 +39,8 @@ defusedxml==0.7.1 # via py-serializable docker==7.1.0 # via moto -faker==28.4.1 - # via -r lambdas/python/purchases/requirements-dev.in +faker==37.12.0 + # via -r requirements-dev.in filelock==3.20.0 # via cachecontrol idna==3.11 @@ -63,8 +63,8 @@ markupsafe==3.0.3 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto[dynamodb,s3]==5.1.15 - # via -r lambdas/python/purchases/requirements-dev.in +moto[dynamodb,s3]==5.1.17 + # via -r requirements-dev.in msgpack==1.1.2 # via cachecontrol packageurl-python==0.17.5 @@ -78,18 +78,18 @@ packaging==25.0 pip-api==0.0.34 # via pip-audit pip-audit==2.9.0 - # via -r lambdas/python/purchases/requirements-dev.in + # via -r requirements-dev.in pip-requirements-parser==32.0.1 # via pip-audit -pip-tools==7.5.1 - # via -r lambdas/python/purchases/requirements-dev.in +pip-tools==7.5.2 + # via -r requirements-dev.in platformdirs==4.5.0 # via pip-audit pluggy==1.6.0 # via # pytest # pytest-cov -py-partiql-parser==0.6.1 +py-partiql-parser==0.6.3 # via moto py-serializable==2.1.0 # via cyclonedx-python-lib @@ -105,16 +105,15 @@ pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==8.4.2 +pytest==9.0.1 # via - # -r lambdas/python/purchases/requirements-dev.in + # -r requirements-dev.in # pytest-cov pytest-cov==7.0.0 - # via -r lambdas/python/purchases/requirements-dev.in + # via -r requirements-dev.in python-dateutil==2.9.0.post0 # via # botocore - # faker # moto pyyaml==6.0.3 # via @@ -131,8 +130,8 @@ responses==0.25.8 # via moto rich==14.2.0 # via pip-audit -ruff==0.14.1 - # via -r lambdas/python/purchases/requirements-dev.in +ruff==0.14.5 + # via -r requirements-dev.in s3transfer==0.14.0 # via boto3 six==1.17.0 @@ -141,6 +140,8 @@ sortedcontainers==2.4.0 # via cyclonedx-python-lib toml==0.10.2 # via pip-audit +tzdata==2025.2 + # via faker urllib3==2.5.0 # via # botocore diff --git a/backend/compact-connect/lambdas/python/purchases/requirements.txt b/backend/compact-connect/lambdas/python/purchases/requirements.txt index 2718af8b8..6141bed67 100644 --- a/backend/compact-connect/lambdas/python/purchases/requirements.txt +++ b/backend/compact-connect/lambdas/python/purchases/requirements.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --no-emit-index-url --no-strip-extras lambdas/python/purchases/requirements.in +# pip-compile requirements.in # authorizenet==1.1.6 - # via -r lambdas/python/purchases/requirements.in -certifi==2025.10.5 + # via -r requirements.in +certifi==2025.11.12 # via requests charset-normalizer==3.4.4 # via requests diff --git a/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt b/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt index afffac437..d4ac196e3 100644 --- a/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt @@ -4,14 +4,14 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/staff-user-pre-token/requirements-dev.in # -boto3==1.40.56 +boto3==1.40.76 # via moto -botocore==1.40.56 +botocore==1.40.76 # via # boto3 # moto # s3transfer -certifi==2025.10.5 +certifi==2025.11.12 # via requests cffi==2.0.0 # via cryptography @@ -33,9 +33,9 @@ markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb,s3]==5.1.15 +moto[dynamodb,s3]==5.1.17 # via -r lambdas/python/staff-user-pre-token/requirements-dev.in -py-partiql-parser==0.6.1 +py-partiql-parser==0.6.3 # via moto pycparser==2.23 # via cffi diff --git a/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt b/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt index 378def7bb..f34018837 100644 --- a/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt @@ -4,14 +4,14 @@ # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/staff-users/requirements-dev.in # -boto3==1.40.56 +boto3==1.40.76 # via moto -botocore==1.40.56 +botocore==1.40.76 # via # boto3 # moto # s3transfer -certifi==2025.10.5 +certifi==2025.11.12 # via requests cffi==2.0.0 # via cryptography @@ -23,7 +23,7 @@ cryptography==46.0.3 # moto docker==7.1.0 # via moto -faker==28.4.1 +faker==37.12.0 # via -r lambdas/python/staff-users/requirements-dev.in idna==3.11 # via requests @@ -33,22 +33,21 @@ jmespath==1.0.1 # via # boto3 # botocore -joserfc==1.4.0 +joserfc==1.4.3 # via moto markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[cognitoidp,dynamodb,s3]==5.1.15 +moto[cognitoidp,dynamodb,s3]==5.1.17 # via -r lambdas/python/staff-users/requirements-dev.in -py-partiql-parser==0.6.1 +py-partiql-parser==0.6.3 # via moto pycparser==2.23 # via cffi python-dateutil==2.9.0.post0 # via # botocore - # faker # moto pyyaml==6.0.3 # via @@ -65,6 +64,8 @@ s3transfer==0.14.0 # via boto3 six==1.17.0 # via python-dateutil +tzdata==2025.2 + # via faker urllib3==2.5.0 # via # botocore diff --git a/backend/compact-connect/requirements-dev.txt b/backend/compact-connect/requirements-dev.txt index 6b3eed0b4..80829872d 100644 --- a/backend/compact-connect/requirements-dev.txt +++ b/backend/compact-connect/requirements-dev.txt @@ -8,17 +8,17 @@ boolean-py==5.0 # via license-expression build==1.3.0 # via pip-tools -cachecontrol[filecache]==0.14.3 +cachecontrol[filecache]==0.14.4 # via # cachecontrol # pip-audit -certifi==2025.10.5 +certifi==2025.11.12 # via requests charset-normalizer==3.4.4 # via requests -click==8.3.0 +click==8.3.1 # via pip-tools -coverage[toml]==7.11.0 +coverage[toml]==7.12.0 # via # -r requirements-dev.in # pytest-cov @@ -26,7 +26,7 @@ cyclonedx-python-lib==9.1.0 # via pip-audit defusedxml==0.7.1 # via py-serializable -faker==28.4.1 +faker==37.12.0 # via -r requirements-dev.in filelock==3.20.0 # via cachecontrol @@ -56,7 +56,7 @@ pip-audit==2.9.0 # via -r requirements-dev.in pip-requirements-parser==32.0.1 # via pip-audit -pip-tools==7.5.1 +pip-tools==7.5.2 # via -r requirements-dev.in platformdirs==4.5.0 # via pip-audit @@ -76,28 +76,26 @@ pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==8.4.2 +pytest==9.0.1 # via # -r requirements-dev.in # pytest-cov pytest-cov==7.0.0 # via -r requirements-dev.in -python-dateutil==2.9.0.post0 - # via faker requests==2.32.5 # via # cachecontrol # pip-audit rich==14.2.0 # via pip-audit -ruff==0.14.1 +ruff==0.14.5 # via -r requirements-dev.in -six==1.17.0 - # via python-dateutil sortedcontainers==2.4.0 # via cyclonedx-python-lib toml==0.10.2 # via pip-audit +tzdata==2025.2 + # via faker urllib3==2.5.0 # via requests wheel==0.45.1 diff --git a/backend/compact-connect/requirements.txt b/backend/compact-connect/requirements.txt index 512097dfd..8f63e3f7e 100644 --- a/backend/compact-connect/requirements.txt +++ b/backend/compact-connect/requirements.txt @@ -12,11 +12,11 @@ aws-cdk-asset-awscli-v1==2.2.242 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.1.0 # via aws-cdk-lib -aws-cdk-aws-lambda-python-alpha==2.220.0a0 +aws-cdk-aws-lambda-python-alpha==2.225.0a0 # via -r requirements.in -aws-cdk-cloud-assembly-schema==48.16.0 +aws-cdk-cloud-assembly-schema==48.20.0 # via aws-cdk-lib -aws-cdk-lib==2.220.0 +aws-cdk-lib==2.225.0 # via # -r requirements.in # aws-cdk-aws-lambda-python-alpha @@ -25,7 +25,7 @@ cattrs==25.3.0 # via jsii cdk-nag==2.37.55 # via -r requirements.in -constructs==10.4.2 +constructs==10.4.3 # via # -r requirements.in # aws-cdk-aws-lambda-python-alpha @@ -33,7 +33,7 @@ constructs==10.4.2 # cdk-nag importlib-resources==6.5.2 # via jsii -jsii==1.117.0 +jsii==1.119.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-node-proxy-agent-v6 @@ -58,7 +58,7 @@ pyyaml==6.0.3 # via -r requirements.in six==1.17.0 # via python-dateutil -typeguard==2.13.3 +typeguard==4.2.1 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-node-proxy-agent-v6 From 22d3d800cd291ae6db1aac35643dfee61dfc6eeb Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Wed, 19 Nov 2025 09:42:31 -0700 Subject: [PATCH 02/14] Move comment to valid location --- backend/compact-connect/bin/sync_deps.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/compact-connect/bin/sync_deps.sh b/backend/compact-connect/bin/sync_deps.sh index 19e960e08..1656a985a 100755 --- a/backend/compact-connect/bin/sync_deps.sh +++ b/backend/compact-connect/bin/sync_deps.sh @@ -20,11 +20,11 @@ pip-sync \ lambdas/python/disaster-recovery/requirements.txt \ lambdas/python/provider-data-v1/requirements-dev.txt \ lambdas/python/provider-data-v1/requirements.txt \ -# We have to manage the purchases lambda Python environment separately -# because it is held back to an older version than the rest of the project -# lambdas/python/purchases/requirements-dev.txt \ -# lambdas/python/purchases/requirements.txt \ lambdas/python/staff-user-pre-token/requirements-dev.txt \ lambdas/python/staff-user-pre-token/requirements.txt \ lambdas/python/staff-users/requirements-dev.txt \ lambdas/python/staff-users/requirements.txt +# We have to manage the purchases lambda Python environment separately +# because it is held back to an older version than the rest of the project +# lambdas/python/purchases/requirements-dev.txt \ +# lambdas/python/purchases/requirements.txt \ From 6e7a3fc6e694a1a35b832a2f413668b35eea7d19 Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Wed, 19 Nov 2025 14:45:47 -0700 Subject: [PATCH 03/14] Remove unneeded example-generation test --- .../compact-connect/app_clients/.gitignore | 2 + .../docs/client_signature_auth.md | 4 +- .../signature_auth_examples.txt | 42 ++-- .../data_model/transaction_client.py | 2 +- .../test_transaction_client.py | 4 +- .../unit/test_signature_auth_integration.py | 180 ------------------ 6 files changed, 28 insertions(+), 206 deletions(-) create mode 100644 backend/compact-connect/app_clients/.gitignore rename backend/compact-connect/{lambdas/python/common/tests/resources => docs}/signature_auth_examples.txt (72%) diff --git a/backend/compact-connect/app_clients/.gitignore b/backend/compact-connect/app_clients/.gitignore new file mode 100644 index 000000000..94060587e --- /dev/null +++ b/backend/compact-connect/app_clients/.gitignore @@ -0,0 +1,2 @@ +*.pem +*.pub diff --git a/backend/compact-connect/docs/client_signature_auth.md b/backend/compact-connect/docs/client_signature_auth.md index bf47096dd..86928cb10 100644 --- a/backend/compact-connect/docs/client_signature_auth.md +++ b/backend/compact-connect/docs/client_signature_auth.md @@ -139,11 +139,9 @@ for *both required and optional signature auth endpoints*). ### Example Signature Implementation -#### Python Example - We maintain an example implementation, which we use to test and validate our own authentication mechanism [here](../lambdas/python/common/common_test/sign_request.py) and some example HTTP request data in a text file -[here](../lambdas/python/common/tests/resources/signature_auth_examples.txt). You can use this as a reference for your +[here](./signature_auth_examples.txt). You can use this as a reference for your own implementation. ### Key Management diff --git a/backend/compact-connect/lambdas/python/common/tests/resources/signature_auth_examples.txt b/backend/compact-connect/docs/signature_auth_examples.txt similarity index 72% rename from backend/compact-connect/lambdas/python/common/tests/resources/signature_auth_examples.txt rename to backend/compact-connect/docs/signature_auth_examples.txt index 06419ba48..b64a3905e 100644 --- a/backend/compact-connect/lambdas/python/common/tests/resources/signature_auth_examples.txt +++ b/backend/compact-connect/docs/signature_auth_examples.txt @@ -21,11 +21,12 @@ GET /v1/compacts/aslp/jurisdictions/al/providers/query?limit=10&offset=0&status= Host: api.example.com Content-Type: application/json User-Agent: CompactConnect-Client/1.0 +Authorization: Bearer X-Algorithm: ECDSA-SHA256 -X-Timestamp: 2025-11-18T20:39:47.299159Z -X-Nonce: 753e75a390774b7faaa8831a77eb6be4 +X-Timestamp: 2025-11-19T21:21:21.242166Z +X-Nonce: d34b90dd39e64c739f9b22070d0433bf X-Key-Id: test-key-001 -X-Signature: MEUCIQDJB+rNp/0vr5x/aXbBIppyi6lHdUMFAfkk6AIQpD277AIgTm2X/CAcFJyO4gU98x0KqfVjwIG9Q1WNVWTs1HDiwZA= +X-Signature: MEYCIQDxGk8KYQskZaiD2XCCvZJBlLz7TXPM8nW7BqcfTTO5ygIhAPtLd+oBgdMHfskhlCjFf/dc2fFzz9jcgaxsxXdA5Ddg Plaintext String to Sign: @@ -33,14 +34,14 @@ Plaintext String to Sign: GET /v1/compacts/aslp/jurisdictions/al/providers/query limit=10&offset=0&status=active -2025-11-18T20:39:47.299159Z -753e75a390774b7faaa8831a77eb6be4 +2025-11-19T21:21:21.242166Z +d34b90dd39e64c739f9b22070d0433bf test-key-001 Base64-Encoded String to Sign: -R0VUCi92MS9jb21wYWN0cy9hc2xwL2p1cmlzZGljdGlvbnMvYWwvcHJvdmlkZXJzL3F1ZXJ5CmxpbWl0PTEwJm9mZnNldD0wJnN0YXR1cz1hY3RpdmUKMjAyNS0xMS0xOFQyMDozOTo0Ny4yOTkxNTlaCjc1M2U3NWEzOTA3NzRiN2ZhYWE4ODMxYTc3ZWI2YmU0CnRlc3Qta2V5LTAwMQ== +R0VUCi92MS9jb21wYWN0cy9hc2xwL2p1cmlzZGljdGlvbnMvYWwvcHJvdmlkZXJzL3F1ZXJ5CmxpbWl0PTEwJm9mZnNldD0wJnN0YXR1cz1hY3RpdmUKMjAyNS0xMS0xOVQyMToyMToyMS4yNDIxNjZaCmQzNGI5MGRkMzllNjRjNzM5ZjliMjIwNzBkMDQzM2JmCnRlc3Qta2V5LTAwMQ== ================================================================================ @@ -54,11 +55,12 @@ POST /v1/compacts/aslp/jurisdictions/al/providers?validate=true HTTP/1.1 Host: api.example.com Content-Type: application/json User-Agent: CompactConnect-Client/1.0 +Authorization: Bearer X-Algorithm: ECDSA-SHA256 -X-Timestamp: 2025-11-18T20:39:47.308195Z -X-Nonce: 7da26c6cb66a4fd2ad137afb4c5e8c4a +X-Timestamp: 2025-11-19T21:21:21.245504Z +X-Nonce: 26cd4a44b74f425d8630d1ea9c98127e X-Key-Id: test-key-002 -X-Signature: MEQCIDI6WiwTS6ZNYEsHRUFCTy8X/Sa5JpfArVM4TOvFujGnAiA1a4Ma6MCU2NweFVsNnB4ZWXUr7NQRdohVPY4feKsMJA== +X-Signature: MEUCIQDoIo1XqJo6X6HTt2CbZTWN1RI5Jex0EFwb9MoLXrKVnQIgV883LXq3fKdiv1hwU98Kt7hBQKO+2hyt8D3bL6GJlDw= Plaintext String to Sign: @@ -66,14 +68,14 @@ Plaintext String to Sign: POST /v1/compacts/aslp/jurisdictions/al/providers validate=true -2025-11-18T20:39:47.308195Z -7da26c6cb66a4fd2ad137afb4c5e8c4a +2025-11-19T21:21:21.245504Z +26cd4a44b74f425d8630d1ea9c98127e test-key-002 Base64-Encoded String to Sign: -UE9TVAovdjEvY29tcGFjdHMvYXNscC9qdXJpc2RpY3Rpb25zL2FsL3Byb3ZpZGVycwp2YWxpZGF0ZT10cnVlCjIwMjUtMTEtMThUMjA6Mzk6NDcuMzA4MTk1Wgo3ZGEyNmM2Y2I2NmE0ZmQyYWQxMzdhZmI0YzVlOGM0YQp0ZXN0LWtleS0wMDI= +UE9TVAovdjEvY29tcGFjdHMvYXNscC9qdXJpc2RpY3Rpb25zL2FsL3Byb3ZpZGVycwp2YWxpZGF0ZT10cnVlCjIwMjUtMTEtMTlUMjE6MjE6MjEuMjQ1NTA0WgoyNmNkNGE0NGI3NGY0MjVkODYzMGQxZWE5Yzk4MTI3ZQp0ZXN0LWtleS0wMDI= ================================================================================ @@ -87,11 +89,12 @@ GET /v1/compacts/aslp/jurisdictions/al/providers/12345 HTTP/1.1 Host: api.example.com Content-Type: application/json User-Agent: CompactConnect-Client/1.0 +Authorization: Bearer X-Algorithm: ECDSA-SHA256 -X-Timestamp: 2025-11-18T20:39:47.308296Z -X-Nonce: bc4340855ef1483aa69bcf91368becbe +X-Timestamp: 2025-11-19T21:21:21.245591Z +X-Nonce: 3e1aa862e0ee4c1e94e44f2ce35a89a7 X-Key-Id: test-key-003 -X-Signature: MEUCIQDOm302tChMchtofb1n9bVNpCPaia/VCWWPrK16Dip8MwIgXH/M+HFoqv69+wo84jmHZnAw1B4IoFU6RQ+B+v9ZS18= +X-Signature: MEYCIQChYjYNjARVQZx53V551i2x6acWcvOF7ipe8pu/tHYwKQIhAISUX+oxCtPBKLOc2tqpJg6FTKX7pR8ULWXSWKO2Ira5 Plaintext String to Sign: @@ -99,14 +102,14 @@ Plaintext String to Sign: GET /v1/compacts/aslp/jurisdictions/al/providers/12345 -2025-11-18T20:39:47.308296Z -bc4340855ef1483aa69bcf91368becbe +2025-11-19T21:21:21.245591Z +3e1aa862e0ee4c1e94e44f2ce35a89a7 test-key-003 Base64-Encoded String to Sign: -R0VUCi92MS9jb21wYWN0cy9hc2xwL2p1cmlzZGljdGlvbnMvYWwvcHJvdmlkZXJzLzEyMzQ1CgoyMDI1LTExLTE4VDIwOjM5OjQ3LjMwODI5NloKYmM0MzQwODU1ZWYxNDgzYWE2OWJjZjkxMzY4YmVjYmUKdGVzdC1rZXktMDAz +R0VUCi92MS9jb21wYWN0cy9hc2xwL2p1cmlzZGljdGlvbnMvYWwvcHJvdmlkZXJzLzEyMzQ1CgoyMDI1LTExLTE5VDIxOjIxOjIxLjI0NTU5MVoKM2UxYWE4NjJlMGVlNGMxZTk0ZTQ0ZjJjZTM1YTg5YTcKdGVzdC1rZXktMDAz ================================================================================ @@ -120,11 +123,12 @@ POST /path?a=1&b=value%20two HTTP/1.1 Host: api.example.com Content-Type: application/json User-Agent: CompactConnect-Client/1.0 +Authorization: Bearer X-Algorithm: ECDSA-SHA256 X-Timestamp: 2025-11-11T19:09:53Z X-Nonce: 54ebdc56-4eae-4627-94e1-11ff27a3ec88 X-Key-Id: eLicenseKey -X-Signature: MEYCIQCXzKEPheBUp2UeDGIY/aN4KDjW0TcedI7QnJBqcjNcXAIhAPSMlZF82qBwzE1trHvkdRvrHPFfqxfnfrlrqpMOCXKU +X-Signature: MEQCIFed8UTChmWcKS6yNtjn5KRNVXbRgwn3RC6NZBMUMKOoAiB2xtyQlPft8Dq24rjz28rK8D7hwsZ3BDy4SYQZrmeeTw== Plaintext String to Sign: diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/transaction_client.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/transaction_client.py index 275a230b2..0ef086eb2 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/transaction_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/transaction_client.py @@ -377,7 +377,7 @@ def reconcile_unsettled_transactions(self, compact: str, settled_transactions: l if unmatched_settled_transaction_ids: logger.error( 'Unable to reconcile some transactions from Authorize.Net with our unsettled transactions', - unreconciled_transactions=unmatched_settled_transaction_ids + unreconciled_transactions=unmatched_settled_transaction_ids, ) for unsettled_tx in unmatched_unsettled: diff --git a/backend/compact-connect/lambdas/python/common/tests/function/test_data_model/test_transaction_client.py b/backend/compact-connect/lambdas/python/common/tests/function/test_data_model/test_transaction_client.py index a525544dd..e72dd93a6 100644 --- a/backend/compact-connect/lambdas/python/common/tests/function/test_data_model/test_transaction_client.py +++ b/backend/compact-connect/lambdas/python/common/tests/function/test_data_model/test_transaction_client.py @@ -261,9 +261,7 @@ def test_reconcile_unsettled_transactions_deletes_matching_record_and_returns_ol ) # Two unmatched transactions remain @patch('cc_common.data_model.transaction_client.logger') - def test_reconcile_unsettled_transactions_logs_error_when_settled_transactions_not_matched( - self, mock_logger - ): + def test_reconcile_unsettled_transactions_logs_error_when_settled_transactions_not_matched(self, mock_logger): """ Test that reconcile_unsettled_transactions logs an error when settled transactions don't match unsettled ones. """ diff --git a/backend/compact-connect/lambdas/python/common/tests/unit/test_signature_auth_integration.py b/backend/compact-connect/lambdas/python/common/tests/unit/test_signature_auth_integration.py index 7b14fa90c..2eb03a27b 100644 --- a/backend/compact-connect/lambdas/python/common/tests/unit/test_signature_auth_integration.py +++ b/backend/compact-connect/lambdas/python/common/tests/unit/test_signature_auth_integration.py @@ -3,7 +3,6 @@ from copy import deepcopy from datetime import UTC, datetime from unittest.mock import patch -from uuid import uuid4 from aws_lambda_powertools.utilities.typing import LambdaContext @@ -235,182 +234,3 @@ def _create_signed_event(self) -> dict: event['headers'].update(headers) return event - - -class TestSignatureAuthSigner(TstLambdas): - def setUp(self): - """Set up test fixtures.""" - super().setUp() - - # Load test keys - with open('tests/resources/client_private_key.pem') as f: - self.private_key_pem = f.read() - - def test_string_to_sign(self): - from common_test.sign_request import get_string_to_sign - # Generate current timestamp and nonce - timestamp = '2025-11-11T19:09:53Z' - nonce = '54ebdc56-4eae-4627-94e1-11ff27a3ec88' - - string_to_sign = get_string_to_sign( - method='POST', - path='/path', - query_params={ - 'a': '1', - 'b': 'value two', - }, - timestamp=timestamp, - nonce=nonce, - key_id='eLicenseKey' - ) - expected = ( - 'POST\n/path\na=1&b=value%20two\n2025-11-11T19:09:53Z\n' - '54ebdc56-4eae-4627-94e1-11ff27a3ec88\neLicenseKey' - ) - self.assertEqual(string_to_sign, expected) - - def test_generate_signature_examples(self): - """Generate example HTTP requests with signature authentication for client documentation.""" - import base64 - from datetime import UTC, datetime - from urllib.parse import quote - - from common_test.sign_request import get_string_to_sign, sign_request - - # Define four example requests with varying methods, paths, and query parameters - examples = [ - { - 'method': 'GET', - 'path': '/v1/compacts/aslp/jurisdictions/al/providers/query', - 'query_params': {'limit': '10', 'offset': '0', 'status': 'active'}, - 'host': 'api.example.com', - 'key_id': 'test-key-001', - }, - { - 'method': 'POST', - 'path': '/v1/compacts/aslp/jurisdictions/al/providers', - 'query_params': {'validate': 'true'}, - 'host': 'api.example.com', - 'key_id': 'test-key-002', - }, - { - 'method': 'GET', - 'path': '/v1/compacts/aslp/jurisdictions/al/providers/12345', - 'query_params': {}, - 'host': 'api.example.com', - 'key_id': 'test-key-003', - }, - { - 'method': 'POST', - 'path': '/path', - 'query_params': {'a': '1', 'b': 'value two'}, - 'host': 'api.example.com', - 'key_id': 'eLicenseKey', - 'timestamp': '2025-11-11T19:09:53Z', - 'nonce': '54ebdc56-4eae-4627-94e1-11ff27a3ec88', - }, - ] - - output_lines = [] - output_lines.append('=' * 80) - output_lines.append('Signature Authentication Examples') - output_lines.append('=' * 80) - output_lines.append('') - output_lines.append('This document provides example HTTP requests demonstrating the') - output_lines.append('CompactConnect signature authentication scheme.') - output_lines.append('') - output_lines.append('Each example includes:') - output_lines.append(' 1. The raw HTTP request with signature headers') - output_lines.append(' 2. The plaintext string that was signed') - output_lines.append(' 3. The base64-encoded string that was signed') - output_lines.append('') - output_lines.append('=' * 80) - output_lines.append('') - - for idx, example in enumerate(examples, 1): - # Generate timestamp and nonce for this example (use provided values if available) - timestamp = example.get('timestamp') or datetime.now(UTC).isoformat().replace('+00:00', 'Z') - nonce = example.get('nonce') or uuid4().hex - - # Get the string to sign - string_to_sign = get_string_to_sign( - method=example['method'], - path=example['path'], - query_params=example['query_params'], - timestamp=timestamp, - nonce=nonce, - key_id=example['key_id'], - ) - - # Sign the request - signature_headers = sign_request( - method=example['method'], - path=example['path'], - query_params=example['query_params'], - timestamp=timestamp, - nonce=nonce, - key_id=example['key_id'], - private_key_pem=self.private_key_pem, - ) - - # Build the query string for the HTTP request (using same format as signature) - if example['query_params']: - sorted_params = '&'.join( - f'{quote(str(k), safe="")}={quote(str(v), safe="")}' - for k, v in sorted(example['query_params'].items()) - ) - query_string = '?' + sorted_params - else: - query_string = '' - - # Format the raw HTTP request - output_lines.append(f'Example {idx}: {example["method"]} {example["path"]}') - output_lines.append('-' * 80) - output_lines.append('') - output_lines.append('Raw HTTP Request:') - output_lines.append('') - output_lines.append(f'{example["method"]} {example["path"]}{query_string} HTTP/1.1') - output_lines.append(f'Host: {example["host"]}') - output_lines.append('Content-Type: application/json') - output_lines.append('User-Agent: CompactConnect-Client/1.0') - output_lines.append(f'X-Algorithm: {signature_headers["X-Algorithm"]}') - output_lines.append(f'X-Timestamp: {signature_headers["X-Timestamp"]}') - output_lines.append(f'X-Nonce: {signature_headers["X-Nonce"]}') - output_lines.append(f'X-Key-Id: {signature_headers["X-Key-Id"]}') - output_lines.append(f'X-Signature: {signature_headers["X-Signature"]}') - output_lines.append('') - output_lines.append('') - - # Add the plaintext string to sign - output_lines.append('Plaintext String to Sign:') - output_lines.append('') - output_lines.append(string_to_sign) - output_lines.append('') - output_lines.append('') - - # Add the base64-encoded string to sign - string_to_sign_b64 = base64.b64encode(string_to_sign.encode()).decode() - output_lines.append('Base64-Encoded String to Sign:') - output_lines.append('') - output_lines.append(string_to_sign_b64) - output_lines.append('') - output_lines.append('') - output_lines.append('=' * 80) - output_lines.append('') - - # Write to file - output_file = 'tests/resources/signature_auth_examples.txt' - with open(output_file, 'w') as f: - f.write('\n'.join(output_lines)) - - # Verify the file was created and has content - with open(output_file) as f: - content = f.read() - self.assertGreater(len(content), 0) - self.assertIn('Example 1:', content) - self.assertIn('Example 2:', content) - self.assertIn('Example 3:', content) - self.assertIn('Example 4:', content) - self.assertIn('Raw HTTP Request:', content) - self.assertIn('Plaintext String to Sign:', content) - self.assertIn('Base64-Encoded String to Sign:', content) From b6cd4e26c46196055b33cfabe8180dfd303c8e81 Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Wed, 19 Nov 2025 16:47:35 -0700 Subject: [PATCH 04/14] Upgrade Python runtimes to 3.14 --- backend/compact-connect/README.md | 22 ++++++++++++++++++- .../common_constructs/python_function.py | 2 +- .../compact-connect/lambdas/nodejs/README.md | 2 +- .../cognito-backup/requirements-dev.txt | 6 ++--- .../python/cognito-backup/requirements.txt | 6 ++--- .../python/common/requirements-dev.txt | 8 +++---- .../lambdas/python/common/requirements.txt | 6 ++--- .../requirements-dev.txt | 6 ++--- .../compact-configuration/requirements.txt | 2 +- .../custom-resources/requirements-dev.txt | 6 ++--- .../python/custom-resources/requirements.txt | 2 +- .../python/data-events/requirements-dev.txt | 6 ++--- .../python/data-events/requirements.txt | 2 +- .../disaster-recovery/requirements-dev.txt | 6 ++--- .../python/disaster-recovery/requirements.txt | 2 +- .../provider-data-v1/requirements-dev.txt | 6 ++--- .../python/provider-data-v1/requirements.txt | 2 +- .../staff-user-pre-token/requirements-dev.txt | 6 ++--- .../staff-user-pre-token/requirements.txt | 2 +- .../python/staff-users/requirements-dev.txt | 6 ++--- .../python/staff-users/requirements.txt | 2 +- backend/compact-connect/requirements-dev.txt | 2 +- backend/compact-connect/requirements.txt | 2 +- .../stacks/api_lambda_stack/provider_users.py | 2 +- .../stacks/persistent_stack/__init__.py | 2 +- .../stacks/persistent_stack/ssn_table.py | 4 +++- backend/compact-connect/tests/app/base.py | 6 ++--- .../test_cognito_user_backup.py | 2 +- .../common_constructs/test_data_migration.py | 2 +- .../test_queue_event_listener.py | 2 +- .../test_queued_lambda_processor.py | 2 +- 31 files changed, 78 insertions(+), 56 deletions(-) diff --git a/backend/compact-connect/README.md b/backend/compact-connect/README.md index 54bf694c1..d49a4b466 100644 --- a/backend/compact-connect/README.md +++ b/backend/compact-connect/README.md @@ -23,9 +23,17 @@ This is an [AWS-CDK](https://aws.amazon.com/cdk/) based project for the backend To deploy this app, you will need: 1) Access to an AWS account -2) Python>=3.13 installed on your machine, preferably through a virtual environment management tool like +2) Python>=3.14 installed on your machine, preferably through a virtual environment management tool like [pyenv](https://github.com/pyenv/pyenv), for clean management of virtual environments across multiple Python versions. + > Note: The [purchases lambda](./lambdas/python/purchases) depends on the + > [Authorize.Net python sdk](https://github.com/AuthorizeNet/sdk-python/issues/164), which is barely maintained at + > present, and is not yet compatible with Python 3.13. Due to that restriction, we have to hold back the python + > version of just this lambda, so that the entire project is not impacted. For local development, this means that, + > at least for this one lambda, developers will have to have a dedicated python environment, held back at Python + > 3.12. That environment and its dependencies will have to be maintained separately from those of the rest of the + > project, which can all share a common virtual environment and common dependencies, without excessive risk of + > version conflicts. 3) Otherwise, follow the [Prerequisites section](https://cdkworkshop.com/15-prerequisites.html) of the CDK workshop to prepare your system to work with AWS-CDK, including a NodeJS install. 4) Follow the steps in the [Installing Dependencies](#installing-dependencies) section. @@ -70,6 +78,18 @@ For development work there are additional requirements in `requirements-dev.txt` To add additional dependencies, for example other CDK libraries, just add them to the `requirements.in` file and rerun `pip-compile requirements.in`, then `pip install -r requirements.txt` command. +### Convenience scripts + +To simplify dependency installation in this project, which includes many runtimes with similar dependencies, maintain +the dependency files with two convenience scripts, which manage the file contents for _most_ runtimes (See Note below), +[compile_requirements.sh](./bin/compile_requirements.sh), and installs the defined dependencies, +[sync_deps.sh](./bin/sync_deps.sh). + +> Note: Due to its dependency on the Authorize.Net python sdk, the [purchases lambda](./lambdas/python/purchases) +> dependencies have to be maintained separately from the rest of the project. You can update the requirements files for +> that lambda directly with the `pip-compile` command, and install dependencies into your python enviornment dedicated +> to that lambda with the `pip-sync` command. + ## Local Development [Back to top](#compact-connect---backend-developer-documentation) diff --git a/backend/compact-connect/common_constructs/python_function.py b/backend/compact-connect/common_constructs/python_function.py index c587d6cac..0dbcf7828 100644 --- a/backend/compact-connect/common_constructs/python_function.py +++ b/backend/compact-connect/common_constructs/python_function.py @@ -29,7 +29,7 @@ def __init__( construct_id: str, *, lambda_dir: str, - runtime: Runtime = Runtime.PYTHON_3_13, + runtime: Runtime = Runtime.PYTHON_3_14, log_retention: RetentionDays = RetentionDays.INFINITE, alarm_topic: ITopic = None, role: IRole = None, diff --git a/backend/compact-connect/lambdas/nodejs/README.md b/backend/compact-connect/lambdas/nodejs/README.md index 2d27942eb..5b6ca8230 100644 --- a/backend/compact-connect/lambdas/nodejs/README.md +++ b/backend/compact-connect/lambdas/nodejs/README.md @@ -4,7 +4,7 @@ This folder contains all lambda runtimes that are written with NodeJS/TypeScript ## Prerequisites -* **[Node](https://github.com/creationix/nvm#installation) `22.X`** +* **[Node](https://github.com/creationix/nvm#installation) `24.X`** * **[Yarn](https://yarnpkg.com/en/) `1.22.22`** * `npm install --global yarn@1.22.22` diff --git a/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt b/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt index 5d4d9e65d..f94e5cec9 100644 --- a/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/cognito-backup/requirements-dev.txt @@ -1,16 +1,16 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/cognito-backup/requirements-dev.in # aws-lambda-powertools==3.23.0 # via -r lambdas/python/cognito-backup/requirements-dev.in -boto3==1.40.76 +boto3==1.41.0 # via # -r lambdas/python/cognito-backup/requirements-dev.in # moto -botocore==1.40.76 +botocore==1.41.0 # via # -r lambdas/python/cognito-backup/requirements-dev.in # boto3 diff --git a/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt b/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt index 44a833959..a58094bd4 100644 --- a/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt +++ b/backend/compact-connect/lambdas/python/cognito-backup/requirements.txt @@ -1,14 +1,14 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/cognito-backup/requirements.in # aws-lambda-powertools==3.23.0 # via -r lambdas/python/cognito-backup/requirements.in -boto3==1.40.76 +boto3==1.41.0 # via -r lambdas/python/cognito-backup/requirements.in -botocore==1.40.76 +botocore==1.41.0 # via # -r lambdas/python/cognito-backup/requirements.in # boto3 diff --git a/backend/compact-connect/lambdas/python/common/requirements-dev.txt b/backend/compact-connect/lambdas/python/common/requirements-dev.txt index b89985fed..4dbfbec7e 100644 --- a/backend/compact-connect/lambdas/python/common/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/common/requirements-dev.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/common/requirements-dev.in @@ -12,11 +12,11 @@ attrs==25.4.0 # via # jsonschema # referencing -aws-sam-translator==1.101.0 +aws-sam-translator==1.102.0 # via cfn-lint aws-xray-sdk==2.15.0 # via moto -boto3==1.40.76 +boto3==1.41.0 # via # aws-sam-translator # moto @@ -24,7 +24,7 @@ boto3-stubs[full]==1.40.76 # via -r lambdas/python/common/requirements-dev.in boto3-stubs-full==1.40.76 # via boto3-stubs -botocore==1.40.76 +botocore==1.41.0 # via # aws-xray-sdk # boto3 diff --git a/backend/compact-connect/lambdas/python/common/requirements.txt b/backend/compact-connect/lambdas/python/common/requirements.txt index b74f5a66a..6f49c3833 100644 --- a/backend/compact-connect/lambdas/python/common/requirements.txt +++ b/backend/compact-connect/lambdas/python/common/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/common/requirements.in @@ -10,9 +10,9 @@ argon2-cffi-bindings==25.1.0 # via argon2-cffi aws-lambda-powertools==3.23.0 # via -r lambdas/python/common/requirements.in -boto3==1.40.76 +boto3==1.41.0 # via -r lambdas/python/common/requirements.in -botocore==1.40.76 +botocore==1.41.0 # via # boto3 # s3transfer diff --git a/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt b/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt index 326b79307..043ab6de2 100644 --- a/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/compact-configuration/requirements-dev.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/compact-configuration/requirements-dev.in # -boto3==1.40.76 +boto3==1.41.0 # via moto -botocore==1.40.76 +botocore==1.41.0 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/compact-configuration/requirements.txt b/backend/compact-connect/lambdas/python/compact-configuration/requirements.txt index 4e0089cfc..79d227dbe 100644 --- a/backend/compact-connect/lambdas/python/compact-configuration/requirements.txt +++ b/backend/compact-connect/lambdas/python/compact-configuration/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/compact-configuration/requirements.in diff --git a/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt b/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt index 5466f25c7..db56d974e 100644 --- a/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/custom-resources/requirements-dev.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/custom-resources/requirements-dev.in # -boto3==1.40.76 +boto3==1.41.0 # via moto -botocore==1.40.76 +botocore==1.41.0 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/custom-resources/requirements.txt b/backend/compact-connect/lambdas/python/custom-resources/requirements.txt index a4578b969..2a8810a14 100644 --- a/backend/compact-connect/lambdas/python/custom-resources/requirements.txt +++ b/backend/compact-connect/lambdas/python/custom-resources/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/custom-resources/requirements.in diff --git a/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt b/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt index 039e3009d..22a836bd7 100644 --- a/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/data-events/requirements-dev.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/data-events/requirements-dev.in # -boto3==1.40.76 +boto3==1.41.0 # via moto -botocore==1.40.76 +botocore==1.41.0 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/data-events/requirements.txt b/backend/compact-connect/lambdas/python/data-events/requirements.txt index 7df16379d..7a1fc37aa 100644 --- a/backend/compact-connect/lambdas/python/data-events/requirements.txt +++ b/backend/compact-connect/lambdas/python/data-events/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/data-events/requirements.in diff --git a/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt b/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt index 002774c92..44d9a0022 100644 --- a/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/disaster-recovery/requirements-dev.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/disaster-recovery/requirements-dev.in # -boto3==1.40.76 +boto3==1.41.0 # via moto -botocore==1.40.76 +botocore==1.41.0 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/disaster-recovery/requirements.txt b/backend/compact-connect/lambdas/python/disaster-recovery/requirements.txt index ffc3ccb09..9ad49d395 100644 --- a/backend/compact-connect/lambdas/python/disaster-recovery/requirements.txt +++ b/backend/compact-connect/lambdas/python/disaster-recovery/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/disaster-recovery/requirements.in diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt b/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt index ac016de3c..35825ad72 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/provider-data-v1/requirements-dev.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/provider-data-v1/requirements-dev.in # -boto3==1.40.76 +boto3==1.41.0 # via moto -botocore==1.40.76 +botocore==1.41.0 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/requirements.txt b/backend/compact-connect/lambdas/python/provider-data-v1/requirements.txt index 5060b3544..f9665c63b 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/requirements.txt +++ b/backend/compact-connect/lambdas/python/provider-data-v1/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/provider-data-v1/requirements.in diff --git a/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt b/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt index d4ac196e3..fa4f50392 100644 --- a/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements-dev.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/staff-user-pre-token/requirements-dev.in # -boto3==1.40.76 +boto3==1.41.0 # via moto -botocore==1.40.76 +botocore==1.41.0 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements.txt b/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements.txt index 43cf8504b..e7cabe5cb 100644 --- a/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements.txt +++ b/backend/compact-connect/lambdas/python/staff-user-pre-token/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/staff-user-pre-token/requirements.in diff --git a/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt b/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt index f34018837..54e10b15f 100644 --- a/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/staff-users/requirements-dev.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/staff-users/requirements-dev.in # -boto3==1.40.76 +boto3==1.41.0 # via moto -botocore==1.40.76 +botocore==1.41.0 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/staff-users/requirements.txt b/backend/compact-connect/lambdas/python/staff-users/requirements.txt index ac407fa42..d32dda42a 100644 --- a/backend/compact-connect/lambdas/python/staff-users/requirements.txt +++ b/backend/compact-connect/lambdas/python/staff-users/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras lambdas/python/staff-users/requirements.in diff --git a/backend/compact-connect/requirements-dev.txt b/backend/compact-connect/requirements-dev.txt index 80829872d..6ea674de6 100644 --- a/backend/compact-connect/requirements-dev.txt +++ b/backend/compact-connect/requirements-dev.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras requirements-dev.in diff --git a/backend/compact-connect/requirements.txt b/backend/compact-connect/requirements.txt index 8f63e3f7e..7ead839a2 100644 --- a/backend/compact-connect/requirements.txt +++ b/backend/compact-connect/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.13 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras requirements.in diff --git a/backend/compact-connect/stacks/api_lambda_stack/provider_users.py b/backend/compact-connect/stacks/api_lambda_stack/provider_users.py index ce2fcdca7..c8aabc816 100644 --- a/backend/compact-connect/stacks/api_lambda_stack/provider_users.py +++ b/backend/compact-connect/stacks/api_lambda_stack/provider_users.py @@ -556,7 +556,7 @@ def _create_dummy_provider_registration_handler(self, scope: Construct): description='Provider registration handler dummy function', handler='handler', code=Code.from_inline('def handler(*args, **kwargs):\n return'), - runtime=Runtime.PYTHON_3_13, + runtime=Runtime.PYTHON_3_14, log_retention=RetentionDays.ONE_DAY, # Triggers creation of the LogRetention custom resource ) # Pin the exports here until the ApiStack clears it from its template diff --git a/backend/compact-connect/stacks/persistent_stack/__init__.py b/backend/compact-connect/stacks/persistent_stack/__init__.py index e6653f802..4909c69bd 100644 --- a/backend/compact-connect/stacks/persistent_stack/__init__.py +++ b/backend/compact-connect/stacks/persistent_stack/__init__.py @@ -64,7 +64,7 @@ def __init__( self.python_common_layer_versions = PythonCommonLayerVersions( self, 'PythonCommonLayerVersions', - compatible_runtimes=[Runtime.PYTHON_3_12, Runtime.PYTHON_3_13], + compatible_runtimes=[Runtime.PYTHON_3_12, Runtime.PYTHON_3_14], ) self.shared_encryption_key = Key( diff --git a/backend/compact-connect/stacks/persistent_stack/ssn_table.py b/backend/compact-connect/stacks/persistent_stack/ssn_table.py index 0afec31b9..d6c7aa8b1 100644 --- a/backend/compact-connect/stacks/persistent_stack/ssn_table.py +++ b/backend/compact-connect/stacks/persistent_stack/ssn_table.py @@ -174,7 +174,9 @@ def __init__( region=stack.region, account=stack.account, resource='table', - resource_name=f'{self.table_name}/index/{self.ssn_index_name}', + # We have to use the constant here, because using `self.table_name` here creates a circular + # reference in the resulting template. + resource_name=f'{SSN_TABLE_NAME}/index/{self.ssn_index_name}', ), ], ) diff --git a/backend/compact-connect/tests/app/base.py b/backend/compact-connect/tests/app/base.py index 7d6ff3a54..122317b2d 100644 --- a/backend/compact-connect/tests/app/base.py +++ b/backend/compact-connect/tests/app/base.py @@ -228,9 +228,9 @@ def _inspect_persistent_stack( self.assertEqual(staff_users_user_pool_app_client['ReadAttributes'], ['email']) self.assertEqual(staff_users_user_pool_app_client['WriteAttributes'], ['email']) - self._inspect_data_events_table(persistent_stack, persistent_stack_template) - self._inspect_ssn_table(persistent_stack, persistent_stack_template) - self._inspect_backup_resources(persistent_stack, persistent_stack_template) + self._inspect_data_events_table(persistent_stack, persistent_stack_template) + self._inspect_ssn_table(persistent_stack, persistent_stack_template) + self._inspect_backup_resources(persistent_stack, persistent_stack_template) def _inspect_ssn_table(self, persistent_stack: PersistentStack, persistent_stack_template: Template): ssn_key_logical_id = persistent_stack.get_logical_id(persistent_stack.ssn_table.key.node.default_child) diff --git a/backend/compact-connect/tests/common_constructs/test_cognito_user_backup.py b/backend/compact-connect/tests/common_constructs/test_cognito_user_backup.py index 2d4732f6e..dc7aa7493 100644 --- a/backend/compact-connect/tests/common_constructs/test_cognito_user_backup.py +++ b/backend/compact-connect/tests/common_constructs/test_cognito_user_backup.py @@ -43,7 +43,7 @@ def setUpClass(cls): PythonCommonLayerVersions( common_stack, 'CommonLayers', - compatible_runtimes=[Runtime.PYTHON_3_13], + compatible_runtimes=[Runtime.PYTHON_3_14], ) cls.stack = Stack(cls.app, 'TestStack') diff --git a/backend/compact-connect/tests/common_constructs/test_data_migration.py b/backend/compact-connect/tests/common_constructs/test_data_migration.py index c907c48cf..aa002bd35 100644 --- a/backend/compact-connect/tests/common_constructs/test_data_migration.py +++ b/backend/compact-connect/tests/common_constructs/test_data_migration.py @@ -29,7 +29,7 @@ def test_data_migration_synthesizes(self): PythonCommonLayerVersions( common_stack, 'CommonLayers', - compatible_runtimes=[Runtime.PYTHON_3_13], + compatible_runtimes=[Runtime.PYTHON_3_14], ) stack = Stack(app, 'Stack') diff --git a/backend/compact-connect/tests/common_constructs/test_queue_event_listener.py b/backend/compact-connect/tests/common_constructs/test_queue_event_listener.py index 4ad5dbba1..ae2fc0d6e 100644 --- a/backend/compact-connect/tests/common_constructs/test_queue_event_listener.py +++ b/backend/compact-connect/tests/common_constructs/test_queue_event_listener.py @@ -25,7 +25,7 @@ def setUp(self): self.stack, 'TestFunction', handler='handle', - runtime=Runtime.PYTHON_3_13, + runtime=Runtime.PYTHON_3_14, code=Code.from_inline("""def handle(*args): return"""), ) diff --git a/backend/compact-connect/tests/common_constructs/test_queued_lambda_processor.py b/backend/compact-connect/tests/common_constructs/test_queued_lambda_processor.py index 5bb9df254..40ad20111 100644 --- a/backend/compact-connect/tests/common_constructs/test_queued_lambda_processor.py +++ b/backend/compact-connect/tests/common_constructs/test_queued_lambda_processor.py @@ -21,7 +21,7 @@ def test_creates_queues_and_event_source(self): stack, 'Function', handler='handle', - runtime=Runtime.PYTHON_3_13, + runtime=Runtime.PYTHON_3_14, code=Code.from_inline("""def handle(*args): return"""), ) processor = QueuedLambdaProcessor( From a8c2f7ae496bfc1f5da8469f91bc615e833d6ce6 Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Thu, 20 Nov 2025 15:11:47 -0700 Subject: [PATCH 05/14] Upgrade backend to Node 24 --- .../check-compact-connect-ui-app.yml | 4 +- .github/workflows/check-compact-connect.yml | 4 +- .../lambdas/nodejs/README.md | 2 +- .../lambdas/nodejs/cloudfront-csp/README.md | 2 +- .../lambdas/nodejs/package.json | 1 - .../lambdas/nodejs/yarn.lock | 332 +----------------- .../frontend_deployment_stack/distribution.py | 2 +- .../common_constructs/nodejs_function.py | 2 +- .../common_constructs/user_pool.py | 4 + .../pipeline/backend_pipeline.py | 2 +- .../stacks/persistent_stack/ssn_table.py | 2 - .../tests/app/test_pipeline.py | 2 +- .../test_cognito_user_backup.py | 2 +- .../snapshots/SSN_TABLE_RESOURCE_POLICY.json | 10 + 14 files changed, 26 insertions(+), 345 deletions(-) diff --git a/.github/workflows/check-compact-connect-ui-app.yml b/.github/workflows/check-compact-connect-ui-app.yml index aaa478073..6f80a5869 100644 --- a/.github/workflows/check-compact-connect-ui-app.yml +++ b/.github/workflows/check-compact-connect-ui-app.yml @@ -48,7 +48,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v1 with: - node-version: '22.1.0' + node-version: '24.11.1' # Use any cached yarn dependencies (saves build time) - uses: actions/cache@v4 @@ -86,7 +86,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v1 with: - node-version: '22.1.0' + node-version: '24.11.1' # Use any cached yarn dependencies (saves build time) - uses: actions/cache@v4 diff --git a/.github/workflows/check-compact-connect.yml b/.github/workflows/check-compact-connect.yml index 0b69755da..721e00211 100644 --- a/.github/workflows/check-compact-connect.yml +++ b/.github/workflows/check-compact-connect.yml @@ -48,7 +48,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v1 with: - node-version: '22.1.0' + node-version: '24.11.1' # Use any cached yarn dependencies (saves build time) - uses: actions/cache@v4 @@ -86,7 +86,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v1 with: - node-version: '22.1.0' + node-version: '24.11.1' # Use any cached yarn dependencies (saves build time) - uses: actions/cache@v4 diff --git a/backend/compact-connect-ui-app/lambdas/nodejs/README.md b/backend/compact-connect-ui-app/lambdas/nodejs/README.md index 20ed86c09..c2d18d152 100644 --- a/backend/compact-connect-ui-app/lambdas/nodejs/README.md +++ b/backend/compact-connect-ui-app/lambdas/nodejs/README.md @@ -4,7 +4,7 @@ This folder contains all lambda runtimes that are written with NodeJS/JavaScript ## Prerequisites -* **[Node](https://github.com/creationix/nvm#installation) `22.X`** +* **[Node](https://github.com/creationix/nvm#installation) `24.X`** * **[Yarn](https://yarnpkg.com/en/) `1.22.22`** * `npm install --global yarn@1.22.22` diff --git a/backend/compact-connect-ui-app/lambdas/nodejs/cloudfront-csp/README.md b/backend/compact-connect-ui-app/lambdas/nodejs/cloudfront-csp/README.md index 38238c536..cc153ca77 100644 --- a/backend/compact-connect-ui-app/lambdas/nodejs/cloudfront-csp/README.md +++ b/backend/compact-connect-ui-app/lambdas/nodejs/cloudfront-csp/README.md @@ -8,7 +8,7 @@ --- ## Prerequisites -* **[Node](https://github.com/creationix/nvm#installation) `22.X`** +* **[Node](https://github.com/creationix/nvm#installation) `24.X`** * **[Yarn](https://yarnpkg.com/en/) `1.22.22`** * `npm install --global yarn@1.22.22` * **[Mocha](https://mochajs.org/) `10.x.x`+** diff --git a/backend/compact-connect-ui-app/lambdas/nodejs/package.json b/backend/compact-connect-ui-app/lambdas/nodejs/package.json index 1c355f7b4..d5b458edd 100644 --- a/backend/compact-connect-ui-app/lambdas/nodejs/package.json +++ b/backend/compact-connect-ui-app/lambdas/nodejs/package.json @@ -29,7 +29,6 @@ "@aws-sdk/client-dynamodb": "^3.682.0", "@aws-sdk/client-s3": "^3.682.0", "@aws-sdk/util-dynamodb": "^3.682.0", - "aws-lambda": "1.0.7", "zod": "^3.23.8" } } diff --git a/backend/compact-connect-ui-app/lambdas/nodejs/yarn.lock b/backend/compact-connect-ui-app/lambdas/nodejs/yarn.lock index 793afb090..ba6e6914d 100644 --- a/backend/compact-connect-ui-app/lambdas/nodejs/yarn.lock +++ b/backend/compact-connect-ui-app/lambdas/nodejs/yarn.lock @@ -1672,13 +1672,6 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -1699,23 +1692,6 @@ async@^3.2.3: resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== - dependencies: - possible-typed-array-names "^1.0.0" - -aws-lambda@1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/aws-lambda/-/aws-lambda-1.0.7.tgz#c6b674df47458b5ecd43ab734899ad2e2d457013" - integrity sha512-9GNFMRrEMG5y3Jvv+V4azWvc+qNWdWLTjDdhf/zgMlz8haaaLWv0xeAIWxz9PuWUBawsVxy0zZotjCdR3Xq+2w== - dependencies: - aws-sdk "^2.814.0" - commander "^3.0.2" - js-yaml "^3.14.1" - watchpack "^2.0.0-beta.10" - aws-sdk-client-mock-jest@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/aws-sdk-client-mock-jest/-/aws-sdk-client-mock-jest-4.1.0.tgz#40a3bdedd8d551cf2a836b77239038c0ca10e25c" @@ -1734,32 +1710,11 @@ aws-sdk-client-mock@^4.1.0: sinon "^18.0.1" tslib "^2.1.0" -aws-sdk@^2.814.0: - version "2.1692.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1692.0.tgz#9dac5f7bfcc5ab45825cc8591b12753aa7d2902c" - integrity sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw== - dependencies: - buffer "4.9.2" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.16.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - util "^0.12.4" - uuid "8.0.0" - xml2js "0.6.2" - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.0.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - binary-extensions@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" @@ -1797,33 +1752,6 @@ browser-stdout@^1.3.1: resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== -buffer@4.9.2: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -call-bind-apply-helpers@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" - integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -call-bind@^1.0.2, call-bind@^1.0.7: - version "1.0.8" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" - set-function-length "^1.2.2" - callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1975,11 +1903,6 @@ commander@^10.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== -commander@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" - integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -2023,15 +1946,6 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -2047,15 +1961,6 @@ dotenv@^16.3.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== -dunder-proto@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.0.tgz#c2fce098b3c8f8899554905f4377b6d85dabaa80" - integrity sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-errors "^1.3.0" - gopd "^1.2.0" - eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -2076,16 +1981,6 @@ enabled@2.0.x: resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== -es-define-property@^1.0.0, es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - esbuild@0.24.0: version "0.24.0" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.0.tgz#f2d470596885fcb2e91c21eb3da3b3c89c0b55e7" @@ -2198,11 +2093,6 @@ espree@^10.0.1, espree@^10.3.0: acorn-jsx "^5.3.2" eslint-visitor-keys "^4.2.0" -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - esquery@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" @@ -2227,11 +2117,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -events@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== - expect@>28.1.3: version "29.7.0" resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" @@ -2315,13 +2200,6 @@ fn.name@1.x.x: resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - foreground-child@^3.1.0: version "3.3.0" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" @@ -2335,11 +2213,6 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -2350,20 +2223,6 @@ get-func-name@^2.0.1, get-func-name@^2.0.2: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== -get-intrinsic@^1.2.4: - version "1.2.5" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.5.tgz#dfe7dd1b30761b464fe51bf4bb00ac7c37b681e7" - integrity sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg== - dependencies: - call-bind-apply-helpers "^1.0.0" - dunder-proto "^1.0.0" - es-define-property "^1.0.1" - es-errors "^1.3.0" - function-bind "^1.1.2" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" @@ -2378,11 +2237,6 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - glob@^10.4.5: version "10.4.5" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" @@ -2400,12 +2254,7 @@ globals@^14.0.0: resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== -gopd@^1.0.1, gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -graceful-fs@^4.1.2, graceful-fs@^4.2.9: +graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -2415,47 +2264,11 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-symbols@^1.0.3, has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -ieee754@1.1.13: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - -ieee754@^1.1.4: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - ignore@^5.2.0: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" @@ -2479,14 +2292,6 @@ inherits@^2.0.1, inherits@^2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-arrayish@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" @@ -2499,11 +2304,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-callable@^1.1.3: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -2514,13 +2314,6 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -2543,23 +2336,11 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-typed-array@^1.1.3: - version "1.1.13" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== - dependencies: - which-typed-array "^1.1.14" - is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== -isarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -2626,24 +2407,11 @@ jest-util@^29.7.0: graceful-fs "^4.2.9" picomatch "^2.2.3" -jmespath@0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" - integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== - js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.14.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -2962,11 +2730,6 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -possible-typed-array-names@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" - integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -2981,21 +2744,11 @@ pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== - punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== - randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -3044,16 +2797,6 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== -sax@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" - integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== - -sax@>=0.6.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" - integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== - serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" @@ -3061,18 +2804,6 @@ serialize-javascript@^6.0.2: dependencies: randombytes "^2.1.0" -set-function-length@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -3114,11 +2845,6 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -3271,59 +2997,16 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -url@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" - integrity sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ== - dependencies: - punycode "1.3.2" - querystring "0.2.0" - util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -util@^0.12.4: - version "0.12.5" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - which-typed-array "^1.1.2" - -uuid@8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" - integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== - uuid@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -watchpack@^2.0.0-beta.10: - version "2.4.2" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" - integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -which-typed-array@^1.1.14, which-typed-array@^1.1.2: - version "1.1.16" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.16.tgz#db4db429c4706feca2f01677a144278e4a8c216b" - integrity sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.2" - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -3394,19 +3077,6 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" -xml2js@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" - integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - -xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" diff --git a/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/distribution.py b/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/distribution.py index b06fd7c41..ce1bff8e0 100644 --- a/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/distribution.py +++ b/backend/compact-connect-ui-app/stacks/frontend_deployment_stack/distribution.py @@ -124,7 +124,7 @@ def __init__( scope, 'CSPFunction', code=Code.from_inline(csp_function_code), - runtime=Runtime.NODEJS_22_X, + runtime=Runtime.NODEJS_24_X, handler='index.handler', ) diff --git a/backend/compact-connect/common_constructs/nodejs_function.py b/backend/compact-connect/common_constructs/nodejs_function.py index b36034e4b..127b16a85 100644 --- a/backend/compact-connect/common_constructs/nodejs_function.py +++ b/backend/compact-connect/common_constructs/nodejs_function.py @@ -75,7 +75,7 @@ def __init__( super().__init__( scope, construct_id, - runtime=Runtime.NODEJS_22_X, + runtime=Runtime.NODEJS_24_X, entry=os.path.join(lambda_dir, 'handler.ts'), deps_lock_file_path=os.path.join(nodejs_dir, 'yarn.lock'), bundling=BundlingOptions( diff --git a/backend/compact-connect/common_constructs/user_pool.py b/backend/compact-connect/common_constructs/user_pool.py index 1bb903bc8..619c118e4 100644 --- a/backend/compact-connect/common_constructs/user_pool.py +++ b/backend/compact-connect/common_constructs/user_pool.py @@ -220,6 +220,10 @@ def add_custom_app_client_domain( stack, f'{stack.node.path}/AWS679f53fac002430cb0da5b7982bd2287/Resource', suppressions=[ + { + 'id': 'AwsSolutions-L1', + 'reason': 'We do not maintain this lambda runtime. It will be updated with future CDK versions' + }, { 'id': 'HIPAA.Security-LambdaDLQ', 'reason': 'This is an AWS-managed custom resource Lambda used only during deployment.' diff --git a/backend/compact-connect/pipeline/backend_pipeline.py b/backend/compact-connect/pipeline/backend_pipeline.py index 065a1e081..fce59b8eb 100644 --- a/backend/compact-connect/pipeline/backend_pipeline.py +++ b/backend/compact-connect/pipeline/backend_pipeline.py @@ -144,7 +144,7 @@ def __init__( { 'phases': { 'install': { - 'runtime-versions': {'python': '3.13', 'nodejs': '22.x'}, + 'runtime-versions': {'python': '3.14', 'nodejs': '24.x'}, } } } diff --git a/backend/compact-connect/stacks/persistent_stack/ssn_table.py b/backend/compact-connect/stacks/persistent_stack/ssn_table.py index d6c7aa8b1..5fc11514e 100644 --- a/backend/compact-connect/stacks/persistent_stack/ssn_table.py +++ b/backend/compact-connect/stacks/persistent_stack/ssn_table.py @@ -161,8 +161,6 @@ def __init__( actions=[ 'dynamodb:GetItem', 'dynamodb:Query', - 'dynamodb:DescribeTable', - 'dynamodb:GetRecords', 'dynamodb:ConditionCheckItem', ], principals=[StarPrincipal()], diff --git a/backend/compact-connect/tests/app/test_pipeline.py b/backend/compact-connect/tests/app/test_pipeline.py index 52c915a19..9004d069a 100644 --- a/backend/compact-connect/tests/app/test_pipeline.py +++ b/backend/compact-connect/tests/app/test_pipeline.py @@ -283,7 +283,7 @@ def test_synth_generates_python_lambda_layer_with_ssm_parameter(self): persistent_stack_template = Template.from_stack(persistent_stack) # Ensure we have a layer and parameter referencing that layer for each expected runtime - for runtime in ['python3.12', 'python3.13']: + for runtime in ['python3.12', 'python3.14']: layers = persistent_stack_template.find_resources( type=CfnLayerVersion.CFN_RESOURCE_TYPE_NAME, props={ diff --git a/backend/compact-connect/tests/common_constructs/test_cognito_user_backup.py b/backend/compact-connect/tests/common_constructs/test_cognito_user_backup.py index dc7aa7493..135d42dda 100644 --- a/backend/compact-connect/tests/common_constructs/test_cognito_user_backup.py +++ b/backend/compact-connect/tests/common_constructs/test_cognito_user_backup.py @@ -141,7 +141,7 @@ def test_creates_lambda_function(self): lambda_props = lambda_functions[lambda_logical_id]['Properties'] # Verify function configuration - self.assertEqual(lambda_props['Runtime'], 'python3.13') + self.assertEqual(lambda_props['Runtime'], 'python3.14') self.assertEqual(lambda_props['Timeout'], 900) # 15 minutes self.assertEqual(lambda_props['MemorySize'], 512) diff --git a/backend/compact-connect/tests/resources/snapshots/SSN_TABLE_RESOURCE_POLICY.json b/backend/compact-connect/tests/resources/snapshots/SSN_TABLE_RESOURCE_POLICY.json index 965db8448..7fddf5e37 100644 --- a/backend/compact-connect/tests/resources/snapshots/SSN_TABLE_RESOURCE_POLICY.json +++ b/backend/compact-connect/tests/resources/snapshots/SSN_TABLE_RESOURCE_POLICY.json @@ -34,6 +34,16 @@ "Effect": "Deny", "Principal": "*", "Resource": "*" + }, + { + "Action": [ + "dynamodb:ConditionCheckItem", + "dynamodb:GetItem", + "dynamodb:Query" + ], + "Effect": "Deny", + "Principal": "*", + "NotResource": "arn:aws:dynamodb:us-east-1:111122223333:table/ssn-table-DataEventsLog/index/ssnIndex" } ], "Version": "2012-10-17" From 6251ef4a43a3434af2da9fc3ed7b783c9a7a4abb Mon Sep 17 00:00:00 2001 From: Justin Frahm Date: Thu, 20 Nov 2025 12:00:05 -0700 Subject: [PATCH 06/14] Remove aws-lambda dependency (#1213) ### Description List - Addressing the Node CVE in an indirect dependency ### Testing List - Code review - Integrate test with some email events in *test* Closes #1205 --- .../lambdas/nodejs/package.json | 1 - .../compact-connect/lambdas/nodejs/yarn.lock | 297 +----------------- 2 files changed, 2 insertions(+), 296 deletions(-) diff --git a/backend/compact-connect/lambdas/nodejs/package.json b/backend/compact-connect/lambdas/nodejs/package.json index 7e77b6590..7276357dc 100644 --- a/backend/compact-connect/lambdas/nodejs/package.json +++ b/backend/compact-connect/lambdas/nodejs/package.json @@ -46,7 +46,6 @@ "@aws-sdk/client-sesv2": "^3.901.0", "@aws-sdk/util-dynamodb": "^3.901.0", "@jusdino-ia/email-builder": "^0.0.9-alpha.3", - "aws-lambda": "1.0.7", "nodemailer": "^7.0.7", "zod": "^3.23.8" } diff --git a/backend/compact-connect/lambdas/nodejs/yarn.lock b/backend/compact-connect/lambdas/nodejs/yarn.lock index 89f1110b3..998a714d5 100644 --- a/backend/compact-connect/lambdas/nodejs/yarn.lock +++ b/backend/compact-connect/lambdas/nodejs/yarn.lock @@ -2631,23 +2631,6 @@ async@^3.2.3: resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== - dependencies: - possible-typed-array-names "^1.0.0" - -aws-lambda@1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/aws-lambda/-/aws-lambda-1.0.7.tgz" - integrity sha512-9GNFMRrEMG5y3Jvv+V4azWvc+qNWdWLTjDdhf/zgMlz8haaaLWv0xeAIWxz9PuWUBawsVxy0zZotjCdR3Xq+2w== - dependencies: - aws-sdk "^2.814.0" - commander "^3.0.2" - js-yaml "^3.14.1" - watchpack "^2.0.0-beta.10" - aws-sdk-client-mock-jest@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/aws-sdk-client-mock-jest/-/aws-sdk-client-mock-jest-4.1.0.tgz" @@ -2666,22 +2649,6 @@ aws-sdk-client-mock@^4.1.0: sinon "^18.0.1" tslib "^2.1.0" -aws-sdk@^2.814.0: - version "2.1692.0" - resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz" - integrity sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw== - dependencies: - buffer "4.9.2" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.16.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - util "^0.12.4" - uuid "8.0.0" - xml2js "0.6.2" - babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz" @@ -2750,11 +2717,6 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.0.2: - version "1.5.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - binary-extensions@^2.0.0: version "2.3.0" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" @@ -2821,33 +2783,6 @@ buffer-from@^1.0.0: resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@4.9.2: - version "4.9.2" - resolved "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -call-bind-apply-helpers@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz" - integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -call-bind@^1.0.2, call-bind@^1.0.7: - version "1.0.8" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" - set-function-length "^1.2.2" - callsites@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" @@ -3038,11 +2973,6 @@ commander@^10.0.1: resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== -commander@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz" - integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -3124,15 +3054,6 @@ deepmerge@^4.2.2: resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" @@ -3188,15 +3109,6 @@ dotenv@^16.3.1: resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz" integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== -dunder-proto@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz" - integrity sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-errors "^1.3.0" - gopd "^1.2.0" - eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" @@ -3246,16 +3158,6 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-define-property@^1.0.0, es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - esbuild@0.24.0: version "0.24.0" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz" @@ -3427,11 +3329,6 @@ esutils@^2.0.2: resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -events@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/events/-/events-1.1.1.tgz" - integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== - execa@^5.0.0: version "5.1.1" resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" @@ -3575,13 +3472,6 @@ fn.name@1.x.x: resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - foreground-child@^3.1.0: version "3.3.0" resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz" @@ -3620,20 +3510,6 @@ get-func-name@^2.0.1, get-func-name@^2.0.2: resolved "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== -get-intrinsic@^1.2.4: - version "1.2.5" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz" - integrity sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg== - dependencies: - call-bind-apply-helpers "^1.0.0" - dunder-proto "^1.0.0" - es-define-property "^1.0.1" - es-errors "^1.3.0" - function-bind "^1.1.2" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" @@ -3665,11 +3541,6 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - glob@^10.4.5: version "10.4.5" resolved "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz" @@ -3704,12 +3575,7 @@ globals@^14.0.0: resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz" integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== -gopd@^1.0.1, gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -graceful-fs@^4.1.2, graceful-fs@^4.2.9: +graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3724,25 +3590,6 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-symbols@^1.0.3, has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - hasown@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" @@ -3775,16 +3622,6 @@ human-signals@^2.1.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -ieee754@1.1.13: - version "1.1.13" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - -ieee754@^1.1.4: - version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - ignore@^5.2.0, ignore@^5.3.1: version "5.3.2" resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" @@ -3824,14 +3661,6 @@ inherits@2, inherits@^2.0.1, inherits@^2.0.3: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" @@ -3849,11 +3678,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-callable@^1.1.3: - version "1.2.7" - resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - is-core-module@^2.13.0: version "2.15.1" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz" @@ -3876,13 +3700,6 @@ is-generator-fn@^2.0.0: resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" @@ -3910,23 +3727,11 @@ is-stream@^2.0.0: resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-typed-array@^1.1.3: - version "1.1.13" - resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== - dependencies: - which-typed-array "^1.1.14" - is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== -isarray@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -4362,17 +4167,12 @@ jest@^29.7.0: import-local "^3.0.2" jest-cli "^29.7.0" -jmespath@0.16.0: - version "0.16.0" - resolved "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz" - integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1, js-yaml@^3.14.1: +js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -4890,11 +4690,6 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -possible-typed-array-names@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz" - integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== - postcss@^8.3.11: version "8.5.6" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" @@ -4926,11 +4721,6 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz" - integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== - punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -4941,11 +4731,6 @@ pure-rand@^6.0.0: resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" - integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" @@ -5069,16 +4854,6 @@ sanitize-html@^2.17.0: parse-srcset "^1.0.2" postcss "^8.3.11" -sax@1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz" - integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== - -sax@>=0.6.0: - version "1.4.1" - resolved "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz" - integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== - scheduler@^0.23.2: version "0.23.2" resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz" @@ -5103,18 +4878,6 @@ serialize-javascript@^6.0.2: dependencies: randombytes "^2.1.0" -set-function-length@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" @@ -5445,35 +5208,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -url@0.10.3: - version "0.10.3" - resolved "https://registry.npmjs.org/url/-/url-0.10.3.tgz" - integrity sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ== - dependencies: - punycode "1.3.2" - querystring "0.2.0" - util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -util@^0.12.4: - version "0.12.5" - resolved "https://registry.npmjs.org/util/-/util-0.12.5.tgz" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - which-typed-array "^1.1.2" - -uuid@8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz" - integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== - v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" @@ -5495,25 +5234,6 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" -watchpack@^2.0.0-beta.10: - version "2.4.2" - resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz" - integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -which-typed-array@^1.1.14, which-typed-array@^1.1.2: - version "1.1.16" - resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.16.tgz" - integrity sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.2" - which@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" @@ -5597,19 +5317,6 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" -xml2js@0.6.2: - version "0.6.2" - resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz" - integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - -xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - y18n@^5.0.5: version "5.0.8" resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" From 1a18c92bc02fab40192561d5873c0a1c7dccb974 Mon Sep 17 00:00:00 2001 From: John Sandoval Date: Thu, 20 Nov 2025 12:28:38 -0700 Subject: [PATCH 07/14] Frontend/investigative info (#1196) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Requirements List - _None_ ### Description List - Added Investigation model - Updated License & Licensee models with related investigation props & methods - Also updated encumber modal with minor layout update from latest designs - Updated network & store layers for API calls to add / update investigations - Updated LicenseeDetail page with banner to indicate a current investigation - Updated the LicenseCard & PrivilegeCard components to support adding & ending investigations - This includes chaining the encumber workflow if selected by the user ### Testing List - `yarn test:unit:all` should run without errors or warnings - `yarn serve` should run without errors or warnings - `yarn build` should run without errors or warnings - Code review - Testing - Login as a staff with state admin permissions - Locate a licensee with licenses / privileges in your admin state - Use the 3-dot menu of the license / privilege to Add investigation - Workflow should match the figma designs - The card Discipline status should now be Investigation - There should be a banner at the top denoting the user is under investigation, matching the figma designs - Use the 3-dot menu of the license / privilege to End investigation - First, choose the no encumbrance option - Ensure the workflow completes and the licensee is no longer shown as under investigation - Use the 3-dot menu to Add investigation - Use the 3-dot menu to End investigation - This time, choose the encumber option and complete the encumber workflow - Ensure the workflow completes and the licensee is no longer shown as under investigation, but the license / privilege is now shown as encumbered - Additionally play with different scenarios for: - Starting an investigation - Ending an investigation with an encumbrance - Ending an investigation without an encumbrance Closes #1132 ## Summary by CodeRabbit * **New Features** * Investigation workflows for licenses and privileges: start/end actions, multi-step modals, per‑investigation selection, encumbrance options, and visible "under investigation" status with a licensee alert. * **Style** * Responsive two‑column/static layout for detail blocks, wider action menus, section dividers, and new yellow alert color palette. * **Accessibility** * Improved modal keyboard focus handling and ARIA live region messaging. * **Localization** * Added English and Spanish translations for investigation flows and alerts. * **Tests** * New unit tests for Investigation model, license/licensee logic, and store actions. * **Chores** * API/mocks and sample data updated to support investigation create/update and WY mock data. --------- Co-authored-by: Justin Frahm --- .../components/LicenseCard/LicenseCard.less | 102 ++++- .../src/components/LicenseCard/LicenseCard.ts | 393 ++++++++++++++++- .../components/LicenseCard/LicenseCard.vue | 292 ++++++++++++- .../PrivilegeCard/PrivilegeCard.less | 104 ++++- .../components/PrivilegeCard/PrivilegeCard.ts | 397 +++++++++++++++++- .../PrivilegeCard/PrivilegeCard.vue | 291 ++++++++++++- webroot/src/locales/en.json | 30 ++ webroot/src/locales/es.json | 30 ++ .../Investigation/Investigation.model.spec.ts | 167 ++++++++ .../Investigation/Investigation.model.ts | 113 +++++ .../src/models/License/License.model.spec.ts | 34 ++ webroot/src/models/License/License.model.ts | 14 + .../models/Licensee/Licensee.model.spec.ts | 61 +++ webroot/src/models/Licensee/Licensee.model.ts | 54 +++ webroot/src/network/data.api.ts | 93 ++++ webroot/src/network/licenseApi/data.api.ts | 142 +++++++ webroot/src/network/mocks/mock.data.api.ts | 81 ++++ webroot/src/network/mocks/mock.data.ts | 111 +++++ .../LicensingDetail/LicensingDetail.less | 16 + .../pages/LicensingDetail/LicensingDetail.ts | 21 + .../pages/LicensingDetail/LicensingDetail.vue | 4 + webroot/src/store/users/users.actions.ts | 120 ++++++ webroot/src/store/users/users.mutations.ts | 60 +++ webroot/src/store/users/users.spec.ts | 288 +++++++++++++ webroot/src/styles.common/_colors.less | 6 + 25 files changed, 2921 insertions(+), 103 deletions(-) create mode 100644 webroot/src/models/Investigation/Investigation.model.spec.ts create mode 100644 webroot/src/models/Investigation/Investigation.model.ts diff --git a/webroot/src/components/LicenseCard/LicenseCard.less b/webroot/src/components/LicenseCard/LicenseCard.less index 29c499cce..3e19bdecf 100644 --- a/webroot/src/components/LicenseCard/LicenseCard.less +++ b/webroot/src/components/LicenseCard/LicenseCard.less @@ -125,7 +125,7 @@ z-index: 1; display: flex; flex-direction: column; - min-width: 16rem; + min-width: 18rem; padding: 0.6rem 0.8rem; border-radius: 12px; background-color: @white; @@ -154,6 +154,22 @@ font-weight: @fontWeight; cursor: default; } + + &.new-section { + margin-top: 0.4rem; + padding-top: 1rem; + + &::before { + position: absolute; + top: 0; + left: 0.8rem; + width: calc(100% - 1.6rem); + height: 1px; + margin-bottom: 0.4rem; + background-color: @lightGrey; + content: ''; + } + } } } } @@ -244,6 +260,16 @@ .form-row { margin-bottom: 1.6rem; + + &.static-container { + display: flex; + flex-direction: column; + + @media @desktopWidth { + flex-direction: row; + margin-bottom: 0.2rem; + } + } } #notes { @@ -251,17 +277,23 @@ border-color: @fontColor; } - .encumber-license-form-input-container { + .encumber-license-form-input-container, + .add-investigation-form-input-container { padding: 1.6rem; border-radius: 8px; background-color: @veryLightGrey; } - .unencumber-row { + .unencumber-row, + .end-investigation-row { display: flex; flex-direction: column; margin-bottom: 3.2rem; + &.end-investigation-row { + margin-bottom: 1.2rem; + } + @media @desktopWidth { flex-direction: row; align-items: flex-end; @@ -272,15 +304,30 @@ margin: 0; } - .unencumber-select { + .unencumber-select, + .end-investigation-select { display: flex; flex-direction: column; - margin-bottom: 0.4rem; padding: 1rem 0.6rem; border: 1px solid @midGrey; border-radius: 12px; cursor: pointer; + &.end-investigation-select { + width: 100%; + padding-top: 1.4rem; + padding-bottom: 1rem; + } + + &.unencumber-select { + margin-bottom: 0.4rem; + + @media @desktopWidth { + width: 65%; + margin-bottom: 0; + } + } + &.selected { border-color: @fontColor; background-color: @veryLightBlue; @@ -291,11 +338,6 @@ cursor: auto; } - @media @desktopWidth { - width: 65%; - margin-bottom: 0; - } - .inactive-category { margin-bottom: 0.6rem; margin-left: 3rem; @@ -307,7 +349,8 @@ padding-left: 3rem; } - .encumbrance-dates { + .encumbrance-dates, + .investigation-dates { padding-left: 3rem; color: darken(@darkGrey, 10%); font-size: @fontSize; @@ -323,13 +366,40 @@ } } - .static-label { - margin-bottom: 0.2rem; - font-weight: @fontWeightBold; + .add-investigation-success-form { + display: flex; + flex-direction: column; + align-items: center; + } + + .static-input { + display: flex; + flex-direction: column; + width: 100%; + + &:not(:last-child) { + margin-bottom: 1.2rem; + } + + @media @desktopWidth { + width: 50%; + } + + .static-label { + margin-bottom: 0.2rem; + font-weight: @fontWeightBold; + font-size: @fontSize; + } + + .static-value { + overflow: hidden; + font-size: @fontSize; + text-overflow: ellipsis; + } } - .static-value { - font-size: 1.7rem; + .info-block { + margin-bottom: 2rem; } .form-field-error { diff --git a/webroot/src/components/LicenseCard/LicenseCard.ts b/webroot/src/components/LicenseCard/LicenseCard.ts index e1355de10..08d979833 100644 --- a/webroot/src/components/LicenseCard/LicenseCard.ts +++ b/webroot/src/components/LicenseCard/LicenseCard.ts @@ -37,6 +37,7 @@ import { Compact } from '@models/Compact/Compact.model'; import { State } from '@/models/State/State.model'; import { StaffUser, CompactPermission } from '@models/StaffUser/StaffUser.model'; import { AdverseAction } from '@/models/AdverseAction/AdverseAction.model'; +import { Investigation } from '@/models/Investigation/Investigation.model'; import { FormInput } from '@/models/FormInput/FormInput.model'; import Joi from 'joi'; import moment from 'moment'; @@ -73,8 +74,15 @@ class LicenseCard extends mixins(MixinForm) { isEncumberLicenseModalSuccess = false; isUnencumberLicenseModalDisplayed = false; isUnencumberLicenseModalSuccess = false; + isAddInvestigationModalDisplayed = false; + isAddInvestigationModalSuccess = false; + isEndInvestigationModalDisplayed = false; + isEndInvestigationModalConfirm = false; + isEndInvestigationModalSuccess = false; encumbranceInputs: Array = []; selectedEncumbrances: Array = []; + investigationInputs: Array = []; + selectedInvestigation: Investigation | null = null; modalErrorMessage = ''; // @@ -214,14 +222,30 @@ class LicenseCard extends mixins(MixinForm) { return this.license?.isEncumbered() || false; } + get isUnderInvestigation(): boolean { + return this.license?.isUnderInvestigation() || false; + } + get disciplineContent(): string { - return (this.isEncumbered) ? this.$t('licensing.encumbered') : this.$t('licensing.noDiscipline'); + let content = this.$t('licensing.noDiscipline'); + + if (this.isEncumbered) { + content = this.$t('licensing.encumbered'); + } else if (this.isUnderInvestigation) { + content = this.$t('licensing.underInvestigationStatus'); + } + + return content; } get adverseActions(): Array { return this.license?.adverseActions || []; } + get investigations(): Array { + return this.license?.investigations || []; + } + get encumberDisciplineOptions(): Array<{ value: string, name: string | ComputedRef }> { const options = this.$tm('licensing.disciplineTypes').map((disciplineType) => ({ value: disciplineType.key, @@ -252,10 +276,26 @@ class LicenseCard extends mixins(MixinForm) { return options; } + get endInvestigationModalTitle(): string { + let modalTitle = this.$t('licensing.confirmLicenseInvestigationEndSelectTitle'); + + if (this.isEndInvestigationModalSuccess) { + modalTitle = ' '; + } else if (this.isEndInvestigationModalConfirm) { + modalTitle = this.$t('licensing.confirmLicenseInvestigationEndTitle'); + } + + return modalTitle; + } + get isUnencumberSubmitEnabled(): boolean { return Boolean(this.isFormValid && !this.isFormLoading && this.selectedEncumbrances.length); } + get isEndInvestigationSubmitEnabled(): boolean { + return Boolean(this.isFormValid && !this.isFormLoading && this.selectedInvestigation); + } + get isMockPopulateEnabled(): boolean { return Boolean(this.$envConfig.isDevelopment); } @@ -268,6 +308,10 @@ class LicenseCard extends mixins(MixinForm) { this.initFormInputsEncumberLicense(); } else if (this.isUnencumberLicenseModalDisplayed) { this.initFormInputsUnencumberLicense(); + } else if (this.isAddInvestigationModalDisplayed) { + this.initFormInputsAddInvestigation(); + } else if (this.isEndInvestigationModalDisplayed) { + this.initFormInputsEndInvestigation(); } } @@ -341,6 +385,38 @@ class LicenseCard extends mixins(MixinForm) { }); } + initFormInputsAddInvestigation(): void { + this.formData = reactive({ + addInvestigationModalContinue: new FormInput({ + isSubmitInput: true, + id: 'submit-modal-continue', + }), + }); + this.watchFormInputs(); + } + + initFormInputsEndInvestigation(): void { + this.formData = reactive({ + endInvestigationModalContinue: new FormInput({ + isSubmitInput: true, + id: 'submit-modal-continue', + }), + }); + + this.investigations.forEach((investigation: Investigation) => { + const investigationId = investigation.id; + const investigationInput = new FormInput({ + id: `end-investigation-data-${investigationId}`, + name: `end-investigation-data-${investigationId}`, + label: this.$t('licensing.investigationStartedOn', { date: investigation.startDateDisplay() }), + isDisabled: Boolean(investigation.endDate), + }); + + this.formData[`end-investigation-data-${investigationId}`] = investigationInput; + this.investigationInputs.push(investigationInput); + }); + } + resetForm(): void { this.isFormLoading = false; this.isFormSuccessful = false; @@ -435,30 +511,60 @@ class LicenseCard extends mixins(MixinForm) { licenseTypeAbbrev } = this; - await this.$store.dispatch(`users/encumberLicenseRequest`, { - compact: compactType, - licenseeId, - licenseState: stateAbbrev, - licenseType: licenseTypeAbbrev.toLowerCase(), - encumbranceType: this.formData.encumberModalDisciplineAction.value, - ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) - ? { - npdbCategories: this.formData.encumberModalNpdbCategories.value, - } - : { - npdbCategory: this.formData.encumberModalNpdbCategory.value, - } - ), - startDate: this.formData.encumberModalStartDate.value, - }).catch((err) => { - this.modalErrorMessage = err?.message || this.$t('common.error'); - this.isFormError = true; - }); + if (this.selectedInvestigation) { + // Submit the encumbrance as part of a selected investigation update + const investigationId = this.selectedInvestigation?.id; + + await this.$store.dispatch(`users/updateInvestigationLicenseRequest`, { + compact: compactType, + licenseeId, + licenseState: stateAbbrev, + licenseType: licenseTypeAbbrev.toLowerCase(), + investigationId, + encumbrance: { + encumbranceType: this.formData.encumberModalDisciplineAction.value, + ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + npdbCategories: this.formData.encumberModalNpdbCategories.value, + } + : { + npdbCategory: this.formData.encumberModalNpdbCategory.value, + } + ), + startDate: this.formData.encumberModalStartDate.value, + }, + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + } else { + // Submit the encumbrance on its own + await this.$store.dispatch(`users/encumberLicenseRequest`, { + compact: compactType, + licenseeId, + licenseState: stateAbbrev, + licenseType: licenseTypeAbbrev.toLowerCase(), + encumbranceType: this.formData.encumberModalDisciplineAction.value, + ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + npdbCategories: this.formData.encumberModalNpdbCategories.value, + } + : { + npdbCategory: this.formData.encumberModalNpdbCategory.value, + } + ), + startDate: this.formData.encumberModalStartDate.value, + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + } if (!this.isFormError) { this.isFormSuccessful = true; await this.$store.dispatch('license/getLicenseeRequest', { compact: compactType, licenseeId }); this.isEncumberLicenseModalSuccess = true; + this.selectedInvestigation = null; } this.endFormLoading(); @@ -624,6 +730,240 @@ class LicenseCard extends mixins(MixinForm) { } } + // ======================================================= + // ADD INVESTIGATION + // ======================================================= + async toggleAddInvestigationModal(): Promise { + this.resetForm(); + this.isAddInvestigationModalDisplayed = !this.isAddInvestigationModalDisplayed; + + if (this.isAddInvestigationModalDisplayed) { + this.initFormInputs(); + } + } + + closeAddInvestigationModal(event?: Event): void { + event?.preventDefault(); + this.isAddInvestigationModalDisplayed = false; + this.isAddInvestigationModalSuccess = false; + } + + focusTrapAddInvestigationModal(event: KeyboardEvent): void { + const { isAddInvestigationModalSuccess } = this; + const firstTabIndex = (isAddInvestigationModalSuccess) + ? document.getElementById('submit-modal-continue') + : document.getElementById('add-investigation-modal-cancel-button'); + let lastTabIndex = document.getElementById('submit-modal-continue'); + + if (!this.isAddInvestigationModalSuccess && (!this.isFormValid || this.isFormLoading)) { + lastTabIndex = document.getElementById('add-investigation-modal-cancel-button'); + } + + if (event.shiftKey) { + // shift + tab to last input + if (document.activeElement === firstTabIndex) { + lastTabIndex?.focus(); + event.preventDefault(); + } + } else if (document.activeElement === lastTabIndex) { + // Tab to first input + firstTabIndex?.focus(); + event.preventDefault(); + } + } + + async submitAddInvestigation(): Promise { + this.validateAll({ asTouched: true }); + + if (this.isFormValid) { + this.startFormLoading(); + this.modalErrorMessage = ''; + + const { + currentCompactType: compactType, + licenseeId, + stateAbbrev, + licenseTypeAbbrev + } = this; + + await this.$store.dispatch(`users/createInvestigationLicenseRequest`, { + compact: compactType, + licenseeId, + licenseState: stateAbbrev, + licenseType: licenseTypeAbbrev.toLowerCase(), + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + + if (!this.isFormError) { + this.isFormSuccessful = true; + await this.$store.dispatch('license/getLicenseeRequest', { compact: compactType, licenseeId }); + this.isAddInvestigationModalSuccess = true; + } + + this.endFormLoading(); + } + } + + // ======================================================= + // END INVESTIGATION + // ======================================================= + clickEndInvestigationItem(investigation: Investigation, event?: PointerEvent | KeyboardEvent): void { + const { srcElement, type } = event || {}; + const investigationId = investigation?.id; + const nodeType = (srcElement as Element)?.nodeName; + + // Handle wrapped checkbox input so that the wrapper events act the same as the nested checkbox input + if (nodeType === 'INPUT') { + if (type === 'keyup') { + event?.preventDefault(); + } + event?.stopPropagation(); + } else if (nodeType === 'LABEL') { + event?.preventDefault(); + } + + if (investigationId) { + const formInput = this.formData[`end-investigation-data-${investigationId}`]; + const existingValue = Boolean(formInput?.value); + + if (formInput) { + formInput.value = !existingValue; + + if (formInput.value) { + this.addEndInvestigationFormData(investigation); + } else { + this.removeEndInvestigationFormData(); + } + } + } + } + + async addEndInvestigationFormData(investigation: Investigation): Promise { + if (investigation) { + this.selectedInvestigation = investigation; + this.investigationInputs.forEach((input: FormInput) => { + if (input.id !== `end-investigation-data-${investigation.id}`) { + input.value = ''; + } + }); + + this.watchFormInputs(); + this.validateAll(); + } + } + + removeEndInvestigationFormData(): void { + this.selectedInvestigation = null; + this.watchFormInputs(); + this.validateAll(); + } + + getFirstEnabledEndInvestigationFormInputId(): string { + const { formData } = this; + const firstEnabledFormInput: string = Object.keys(formData) + .filter((key) => key !== 'endInvestigationModalContinue') + .find((key) => !formData[key].isDisabled) || ''; + const firstEnabledInputId = formData[firstEnabledFormInput]?.id || 'end-investigation-modal-cancel-button'; + + return firstEnabledInputId; + } + + async toggleEndInvestigationModal(): Promise { + this.resetForm(); + this.isEndInvestigationModalDisplayed = !this.isEndInvestigationModalDisplayed; + + if (this.isEndInvestigationModalDisplayed) { + this.initFormInputs(); + } + } + + closeEndInvestigationModal(event?: Event, keepSelectedInvestigation = false): void { + event?.preventDefault(); + + if (!keepSelectedInvestigation) { + this.selectedInvestigation = null; + } + + this.isEndInvestigationModalDisplayed = false; + this.isEndInvestigationModalConfirm = false; + this.isEndInvestigationModalSuccess = false; + } + + focusTrapEndInvestigationModal(event: KeyboardEvent): void { + const { + isEndInvestigationModalConfirm, + isEndInvestigationModalSuccess, + isEndInvestigationSubmitEnabled + } = this; + const firstEnabledInputId = (isEndInvestigationModalConfirm || isEndInvestigationModalSuccess) + ? 'end-investigation-modal-cancel-button' + : this.getFirstEnabledEndInvestigationFormInputId(); + const firstTabIndex = document.getElementById(firstEnabledInputId); + const lastTabIndex = (isEndInvestigationSubmitEnabled && !isEndInvestigationModalSuccess) + ? document.getElementById('submit-modal-continue') + : document.getElementById('end-investigation-modal-cancel-button'); + + if (event.shiftKey) { + // shift + tab to last input + if (document.activeElement === firstTabIndex) { + lastTabIndex?.focus(); + event.preventDefault(); + } + } else if (document.activeElement === lastTabIndex) { + // Tab to first input + firstTabIndex?.focus(); + event.preventDefault(); + } + } + + continueToEndInvestigationConfirm(): void { + this.isEndInvestigationModalConfirm = true; + } + + submitEndInvestigationWithEncumbrance(): void { + this.closeEndInvestigationModal(undefined, true); + this.toggleEncumberLicenseModal(); + } + + async submitEndInvestigationWithoutEncumbrance(): Promise { + this.validateAll({ asTouched: true }); + + if (this.isFormValid) { + this.startFormLoading(); + this.modalErrorMessage = ''; + + const { + currentCompactType: compactType, + licenseeId, + stateAbbrev, + licenseTypeAbbrev, + } = this; + const investigationId = this.selectedInvestigation?.id; + + await this.$store.dispatch(`users/updateInvestigationLicenseRequest`, { + compact: compactType, + licenseeId, + licenseState: stateAbbrev, + licenseType: licenseTypeAbbrev.toLowerCase(), + investigationId, + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + + if (!this.isFormError) { + this.isFormSuccessful = true; + await this.$store.dispatch('license/getLicenseeRequest', { compact: compactType, licenseeId }); + this.isEndInvestigationModalConfirm = false; + this.isEndInvestigationModalSuccess = true; + } + + this.endFormLoading(); + } + } + async focusTrapTeleportedDatepicker(formInput: FormInput, isOpen: boolean): Promise { if (isOpen) { await nextTick(); @@ -637,6 +977,10 @@ class LicenseCard extends mixins(MixinForm) { return this.selectedEncumbrances.some((selected: AdverseAction) => selected.id === adverseAction.id); } + isInvestigationSelected(investigation: Investigation): boolean { + return this.selectedInvestigation?.id === investigation.id; + } + dateDisplayFormat(unformattedDate: string): string { return dateDisplay(unformattedDate); } @@ -669,6 +1013,15 @@ class LicenseCard extends mixins(MixinForm) { })); await nextTick(); this.validateAll({ asTouched: true }); + } else if (this.isEndInvestigationModalDisplayed) { + await Promise.all(this.investigations + .filter((investigation) => !investigation.hasEndDate()) + .map(async (investigation) => { + this.clickEndInvestigationItem(investigation); + await nextTick(); + })); + await nextTick(); + this.validateAll({ asTouched: true }); } } } diff --git a/webroot/src/components/LicenseCard/LicenseCard.vue b/webroot/src/components/LicenseCard/LicenseCard.vue index 8341eb36b..a0839c1d1 100644 --- a/webroot/src/components/LicenseCard/LicenseCard.vue +++ b/webroot/src/components/LicenseCard/LicenseCard.vue @@ -54,6 +54,26 @@ > {{ $t('licensing.unencumber') }} +
  • + {{ $t('licensing.addInvestigation') }} +
  • +
  • + {{ $t('licensing.endInvestigation') }} +
  • @@ -111,21 +131,25 @@ :isEnabled="isMockPopulateEnabled" @selected="mockPopulate" /> -
    -
    {{ $t('licensing.practitionerName') }}
    -
    {{ licenseeName }}
    -
    -
    -
    {{ $t('common.state') }}
    -
    {{ stateContent }}
    -
    -
    -
    {{ $t('licensing.licenseNumber') }}
    -
    {{ licenseNumber }}
    +
    +
    +
    {{ $t('licensing.practitionerName') }}
    +
    {{ licenseeName }}
    +
    +
    +
    {{ $t('common.state') }}
    +
    {{ stateContent }}
    +
    -
    -
    {{ $t('licensing.licenseType') }}
    -
    {{ licenseTypeAbbrev }}
    +
    +
    +
    {{ $t('licensing.licenseNumber') }}
    +
    {{ licenseNumber }}
    +
    +
    +
    {{ $t('licensing.licenseType') }}
    +
    {{ licenseTypeAbbrev }}
    +
    @@ -325,6 +349,246 @@
    + + + + + +
    diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.less b/webroot/src/components/PrivilegeCard/PrivilegeCard.less index 8db594b95..7cf308a69 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.less +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.less @@ -80,7 +80,7 @@ z-index: 1; display: flex; flex-direction: column; - min-width: 16rem; + min-width: 18rem; padding: 0.6rem 0.8rem; border-radius: 12px; background-color: @white; @@ -109,6 +109,22 @@ font-weight: @fontWeight; cursor: default; } + + &.new-section { + margin-top: 0.4rem; + padding-top: 1rem; + + &::before { + position: absolute; + top: 0; + left: 0.8rem; + width: calc(100% - 1.6rem); + height: 1px; + margin-bottom: 0.4rem; + background-color: @lightGrey; + content: ''; + } + } } } } @@ -165,7 +181,7 @@ :deep(.modal-container) { width: 95%; - max-width: 60rem; + max-width: 62rem; padding: 2rem; @media @tabletWidth { @@ -178,6 +194,16 @@ .form-row { margin-bottom: 1.6rem; + + &.static-container { + display: flex; + flex-direction: column; + + @media @desktopWidth { + flex-direction: row; + margin-bottom: 0.2rem; + } + } } #notes { @@ -185,17 +211,23 @@ border-color: @fontColor; } - .encumber-privilege-form-input-container { + .encumber-privilege-form-input-container, + .add-investigation-form-input-container { padding: 1.6rem; border-radius: 8px; background-color: @veryLightGrey; } - .unencumber-row { + .unencumber-row, + .end-investigation-row { display: flex; flex-direction: column; margin-bottom: 3.2rem; + &.end-investigation-row { + margin-bottom: 1.2rem; + } + @media @desktopWidth { flex-direction: row; align-items: flex-end; @@ -206,15 +238,30 @@ margin: 0; } - .unencumber-select { + .unencumber-select, + .end-investigation-select { display: flex; flex-direction: column; - margin-bottom: 0.4rem; padding: 1rem 0.6rem; border: 1px solid @midGrey; border-radius: 12px; cursor: pointer; + &.end-investigation-select { + width: 100%; + padding-top: 1.4rem; + padding-bottom: 1rem; + } + + &.unencumber-select { + margin-bottom: 0.4rem; + + @media @desktopWidth { + width: 65%; + margin-bottom: 0; + } + } + &.selected { border-color: @fontColor; background-color: @veryLightBlue; @@ -225,11 +272,6 @@ cursor: auto; } - @media @desktopWidth { - width: 65%; - margin-bottom: 0; - } - .inactive-category { margin-bottom: 0.6rem; margin-left: 3rem; @@ -241,7 +283,8 @@ padding-left: 3rem; } - .encumbrance-dates { + .encumbrance-dates, + .investigation-dates { padding-left: 3rem; color: darken(@darkGrey, 10%); font-size: @fontSize; @@ -257,13 +300,40 @@ } } - .static-label { - margin-bottom: 0.2rem; - font-weight: @fontWeightBold; + .add-investigation-success-form { + display: flex; + flex-direction: column; + align-items: center; + } + + .static-input { + display: flex; + flex-direction: column; + width: 100%; + + &:not(:last-child) { + margin-bottom: 1.2rem; + } + + @media @desktopWidth { + width: 50%; + } + + .static-label { + margin-bottom: 0.2rem; + font-weight: @fontWeightBold; + font-size: @fontSize; + } + + .static-value { + overflow: hidden; + font-size: @fontSize; + text-overflow: ellipsis; + } } - .static-value { - font-size: 1.7rem; + .info-block { + margin-bottom: 2rem; } .form-field-error { diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.ts b/webroot/src/components/PrivilegeCard/PrivilegeCard.ts index b29d1b99e..5aa21959d 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.ts +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.ts @@ -35,6 +35,7 @@ import { Compact } from '@models/Compact/Compact.model'; import { State } from '@/models/State/State.model'; import { StaffUser, CompactPermission } from '@models/StaffUser/StaffUser.model'; import { AdverseAction } from '@/models/AdverseAction/AdverseAction.model'; +import { Investigation } from '@/models/Investigation/Investigation.model'; import { FormInput } from '@/models/FormInput/FormInput.model'; import Joi from 'joi'; import moment from 'moment'; @@ -68,8 +69,15 @@ class PrivilegeCard extends mixins(MixinForm) { isEncumberPrivilegeModalSuccess = false; isUnencumberPrivilegeModalDisplayed = false; isUnencumberPrivilegeModalSuccess = false; + isAddInvestigationModalDisplayed = false; + isAddInvestigationModalSuccess = false; + isEndInvestigationModalDisplayed = false; + isEndInvestigationModalConfirm = false; + isEndInvestigationModalSuccess = false; encumbranceInputs: Array = []; selectedEncumbrances: Array = []; + investigationInputs: Array = []; + selectedInvestigation: Investigation | null = null; modalErrorMessage = ''; // @@ -186,14 +194,30 @@ class PrivilegeCard extends mixins(MixinForm) { return this.privilege?.isEncumbered() || false; } + get isUnderInvestigation(): boolean { + return this.privilege?.isUnderInvestigation() || false; + } + get disciplineContent(): string { - return (this.isEncumbered) ? this.$t('licensing.encumbered') : this.$t('licensing.noDiscipline'); + let content = this.$t('licensing.noDiscipline'); + + if (this.isEncumbered) { + content = this.$t('licensing.encumbered'); + } else if (this.isUnderInvestigation) { + content = this.$t('licensing.underInvestigationStatus'); + } + + return content; } get adverseActions(): Array { return this.privilege?.adverseActions || []; } + get investigations(): Array { + return this.privilege?.investigations || []; + } + get encumberDisciplineOptions(): Array<{ value: string, name: string | ComputedRef }> { const options = this.$tm('licensing.disciplineTypes').map((disciplineType) => ({ value: disciplineType.key, @@ -224,10 +248,26 @@ class PrivilegeCard extends mixins(MixinForm) { return options; } + get endInvestigationModalTitle(): string { + let modalTitle = this.$t('licensing.confirmLicenseInvestigationEndSelectTitle'); + + if (this.isEndInvestigationModalSuccess) { + modalTitle = ' '; + } else if (this.isEndInvestigationModalConfirm) { + modalTitle = this.$t('licensing.confirmLicenseInvestigationEndTitle'); + } + + return modalTitle; + } + get isUnencumberSubmitEnabled(): boolean { return Boolean(this.isFormValid && !this.isFormLoading && this.selectedEncumbrances.length); } + get isEndInvestigationSubmitEnabled(): boolean { + return Boolean(this.isFormValid && !this.isFormLoading && this.selectedInvestigation); + } + get isMockPopulateEnabled(): boolean { return Boolean(this.$envConfig.isDevelopment); } @@ -242,6 +282,10 @@ class PrivilegeCard extends mixins(MixinForm) { this.initFormInputsEncumberPrivilege(); } else if (this.isUnencumberPrivilegeModalDisplayed) { this.initFormInputsUnencumberPrivilege(); + } else if (this.isAddInvestigationModalDisplayed) { + this.initFormInputsAddInvestigation(); + } else if (this.isEndInvestigationModalDisplayed) { + this.initFormInputsEndInvestigation(); } } @@ -333,6 +377,38 @@ class PrivilegeCard extends mixins(MixinForm) { }); } + initFormInputsAddInvestigation(): void { + this.formData = reactive({ + addInvestigationModalContinue: new FormInput({ + isSubmitInput: true, + id: 'submit-modal-continue', + }), + }); + this.watchFormInputs(); + } + + initFormInputsEndInvestigation(): void { + this.formData = reactive({ + endInvestigationModalContinue: new FormInput({ + isSubmitInput: true, + id: 'submit-modal-continue', + }), + }); + + this.investigations.forEach((investigation: Investigation) => { + const investigationId = investigation.id; + const investigationInput = new FormInput({ + id: `end-investigation-data-${investigationId}`, + name: `end-investigation-data-${investigationId}`, + label: this.$t('licensing.investigationStartedOn', { date: investigation.startDateDisplay() }), + isDisabled: Boolean(investigation.endDate), + }); + + this.formData[`end-investigation-data-${investigationId}`] = investigationInput; + this.investigationInputs.push(investigationInput); + }); + } + resetForm(): void { this.isFormLoading = false; this.isFormSuccessful = false; @@ -454,6 +530,7 @@ class PrivilegeCard extends mixins(MixinForm) { event?.preventDefault(); this.isEncumberPrivilegeModalDisplayed = false; this.isEncumberPrivilegeModalSuccess = false; + this.selectedInvestigation = null; } focusTrapEncumberPrivilegeModal(event: KeyboardEvent): void { @@ -490,25 +567,54 @@ class PrivilegeCard extends mixins(MixinForm) { privilegeTypeAbbrev } = this; - await this.$store.dispatch(`users/encumberPrivilegeRequest`, { - compact: compactType, - licenseeId, - privilegeState: stateAbbrev, - licenseType: privilegeTypeAbbrev.toLowerCase(), - encumbranceType: this.formData.encumberModalDisciplineAction.value, - ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) - ? { - npdbCategories: this.formData.encumberModalNpdbCategories.value, - } - : { - npdbCategory: this.formData.encumberModalNpdbCategory.value, - } - ), - startDate: this.formData.encumberModalStartDate.value, - }).catch((err) => { - this.modalErrorMessage = err?.message || this.$t('common.error'); - this.isFormError = true; - }); + if (this.selectedInvestigation) { + // Submit the encumbrance as part of a selected investigation update + const investigationId = this.selectedInvestigation?.id; + + await this.$store.dispatch(`users/updateInvestigationPrivilegeRequest`, { + compact: compactType, + licenseeId, + privilegeState: stateAbbrev, + licenseType: privilegeTypeAbbrev.toLowerCase(), + investigationId, + encumbrance: { + encumbranceType: this.formData.encumberModalDisciplineAction.value, + ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + npdbCategories: this.formData.encumberModalNpdbCategories.value, + } + : { + npdbCategory: this.formData.encumberModalNpdbCategory.value, + } + ), + startDate: this.formData.encumberModalStartDate.value, + }, + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + } else { + // Submit the encumbrance on its own + await this.$store.dispatch(`users/encumberPrivilegeRequest`, { + compact: compactType, + licenseeId, + privilegeState: stateAbbrev, + licenseType: privilegeTypeAbbrev.toLowerCase(), + encumbranceType: this.formData.encumberModalDisciplineAction.value, + ...(this.$features.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + npdbCategories: this.formData.encumberModalNpdbCategories.value, + } + : { + npdbCategory: this.formData.encumberModalNpdbCategory.value, + } + ), + startDate: this.formData.encumberModalStartDate.value, + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + } if (!this.isFormError) { this.isFormSuccessful = true; @@ -587,7 +693,7 @@ class PrivilegeCard extends mixins(MixinForm) { this.validateAll(); } - getFirstEnabledFormInputId(): string { + getFirstEnabledUnencumberFormInputId(): string { const { formData } = this; const firstEnabledFormInput: string = Object.keys(formData) .filter((key) => key !== 'unencumberModalContinue') @@ -615,7 +721,7 @@ class PrivilegeCard extends mixins(MixinForm) { focusTrapUnencumberPrivilegeModal(event: KeyboardEvent): void { const { isUnencumberSubmitEnabled } = this; - const firstEnabledInputId = this.getFirstEnabledFormInputId(); + const firstEnabledInputId = this.getFirstEnabledUnencumberFormInputId(); const firstTabIndex = document.getElementById(firstEnabledInputId); const lastTabIndex = (isUnencumberSubmitEnabled) ? document.getElementById('submit-modal-continue') @@ -679,6 +785,240 @@ class PrivilegeCard extends mixins(MixinForm) { } } + // ======================================================= + // ADD INVESTIGATION + // ======================================================= + async toggleAddInvestigationModal(): Promise { + this.resetForm(); + this.isAddInvestigationModalDisplayed = !this.isAddInvestigationModalDisplayed; + + if (this.isAddInvestigationModalDisplayed) { + this.initFormInputs(); + } + } + + closeAddInvestigationModal(event?: Event): void { + event?.preventDefault(); + this.isAddInvestigationModalDisplayed = false; + this.isAddInvestigationModalSuccess = false; + } + + focusTrapAddInvestigationModal(event: KeyboardEvent): void { + const { isAddInvestigationModalSuccess } = this; + const firstTabIndex = (isAddInvestigationModalSuccess) + ? document.getElementById('submit-modal-continue') + : document.getElementById('add-investigation-modal-cancel-button'); + let lastTabIndex = document.getElementById('submit-modal-continue'); + + if (!this.isAddInvestigationModalSuccess && (!this.isFormValid || this.isFormLoading)) { + lastTabIndex = document.getElementById('add-investigation-modal-cancel-button'); + } + + if (event.shiftKey) { + // shift + tab to last input + if (document.activeElement === firstTabIndex) { + lastTabIndex?.focus(); + event.preventDefault(); + } + } else if (document.activeElement === lastTabIndex) { + // Tab to first input + firstTabIndex?.focus(); + event.preventDefault(); + } + } + + async submitAddInvestigation(): Promise { + this.validateAll({ asTouched: true }); + + if (this.isFormValid) { + this.startFormLoading(); + this.modalErrorMessage = ''; + + const { + currentCompactType: compactType, + licenseeId, + stateAbbrev, + privilegeTypeAbbrev + } = this; + + await this.$store.dispatch(`users/createInvestigationPrivilegeRequest`, { + compact: compactType, + licenseeId, + privilegeState: stateAbbrev, + licenseType: privilegeTypeAbbrev.toLowerCase(), + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + + if (!this.isFormError) { + this.isFormSuccessful = true; + await this.$store.dispatch('license/getLicenseeRequest', { compact: compactType, licenseeId }); + this.isAddInvestigationModalSuccess = true; + } + + this.endFormLoading(); + } + } + + // ======================================================= + // END INVESTIGATION + // ======================================================= + clickEndInvestigationItem(investigation: Investigation, event?: PointerEvent | KeyboardEvent): void { + const { srcElement, type } = event || {}; + const investigationId = investigation?.id; + const nodeType = (srcElement as Element)?.nodeName; + + // Handle wrapped checkbox input so that the wrapper events act the same as the nested checkbox input + if (nodeType === 'INPUT') { + if (type === 'keyup') { + event?.preventDefault(); + } + event?.stopPropagation(); + } else if (nodeType === 'LABEL') { + event?.preventDefault(); + } + + if (investigationId) { + const formInput = this.formData[`end-investigation-data-${investigationId}`]; + const existingValue = Boolean(formInput?.value); + + if (formInput) { + formInput.value = !existingValue; + + if (formInput.value) { + this.addEndInvestigationFormData(investigation); + } else { + this.removeEndInvestigationFormData(); + } + } + } + } + + async addEndInvestigationFormData(investigation: Investigation): Promise { + if (investigation) { + this.selectedInvestigation = investigation; + this.investigationInputs.forEach((input: FormInput) => { + if (input.id !== `end-investigation-data-${investigation.id}`) { + input.value = ''; + } + }); + + this.watchFormInputs(); + this.validateAll(); + } + } + + removeEndInvestigationFormData(): void { + this.selectedInvestigation = null; + this.watchFormInputs(); + this.validateAll(); + } + + getFirstEnabledEndInvestigationFormInputId(): string { + const { formData } = this; + const firstEnabledFormInput: string = Object.keys(formData) + .filter((key) => key !== 'endInvestigationModalContinue') + .find((key) => !formData[key].isDisabled) || ''; + const firstEnabledInputId = formData[firstEnabledFormInput]?.id || 'end-investigation-modal-cancel-button'; + + return firstEnabledInputId; + } + + async toggleEndInvestigationModal(): Promise { + this.resetForm(); + this.isEndInvestigationModalDisplayed = !this.isEndInvestigationModalDisplayed; + + if (this.isEndInvestigationModalDisplayed) { + this.initFormInputs(); + } + } + + closeEndInvestigationModal(event?: Event, keepSelectedInvestigation = false): void { + event?.preventDefault(); + + if (!keepSelectedInvestigation) { + this.selectedInvestigation = null; + } + + this.isEndInvestigationModalDisplayed = false; + this.isEndInvestigationModalConfirm = false; + this.isEndInvestigationModalSuccess = false; + } + + focusTrapEndInvestigationModal(event: KeyboardEvent): void { + const { + isEndInvestigationModalConfirm, + isEndInvestigationModalSuccess, + isEndInvestigationSubmitEnabled + } = this; + const firstEnabledInputId = (isEndInvestigationModalConfirm || isEndInvestigationModalSuccess) + ? 'end-investigation-modal-cancel-button' + : this.getFirstEnabledEndInvestigationFormInputId(); + const firstTabIndex = document.getElementById(firstEnabledInputId); + const lastTabIndex = (isEndInvestigationSubmitEnabled && !isEndInvestigationModalSuccess) + ? document.getElementById('submit-modal-continue') + : document.getElementById('end-investigation-modal-cancel-button'); + + if (event.shiftKey) { + // shift + tab to last input + if (document.activeElement === firstTabIndex) { + lastTabIndex?.focus(); + event.preventDefault(); + } + } else if (document.activeElement === lastTabIndex) { + // Tab to first input + firstTabIndex?.focus(); + event.preventDefault(); + } + } + + continueToEndInvestigationConfirm(): void { + this.isEndInvestigationModalConfirm = true; + } + + submitEndInvestigationWithEncumbrance(): void { + this.closeEndInvestigationModal(undefined, true); + this.toggleEncumberPrivilegeModal(); + } + + async submitEndInvestigationWithoutEncumbrance(): Promise { + this.validateAll({ asTouched: true }); + + if (this.isFormValid) { + this.startFormLoading(); + this.modalErrorMessage = ''; + + const { + currentCompactType: compactType, + licenseeId, + stateAbbrev, + privilegeTypeAbbrev, + } = this; + const investigationId = this.selectedInvestigation?.id; + + await this.$store.dispatch(`users/updateInvestigationPrivilegeRequest`, { + compact: compactType, + licenseeId, + privilegeState: stateAbbrev, + licenseType: privilegeTypeAbbrev.toLowerCase(), + investigationId, + }).catch((err) => { + this.modalErrorMessage = err?.message || this.$t('common.error'); + this.isFormError = true; + }); + + if (!this.isFormError) { + this.isFormSuccessful = true; + await this.$store.dispatch('license/getLicenseeRequest', { compact: compactType, licenseeId }); + this.isEndInvestigationModalConfirm = false; + this.isEndInvestigationModalSuccess = true; + } + + this.endFormLoading(); + } + } + async focusTrapTeleportedDatepicker(formInput: FormInput, isOpen: boolean): Promise { if (isOpen) { await nextTick(); @@ -692,6 +1032,10 @@ class PrivilegeCard extends mixins(MixinForm) { return this.selectedEncumbrances.some((selected: AdverseAction) => selected.id === adverseAction.id); } + isInvestigationSelected(investigation: Investigation): boolean { + return this.selectedInvestigation?.id === investigation.id; + } + dateDisplayFormat(unformattedDate: string): string { return dateDisplay(unformattedDate); } @@ -724,6 +1068,15 @@ class PrivilegeCard extends mixins(MixinForm) { })); await nextTick(); this.validateAll({ asTouched: true }); + } else if (this.isEndInvestigationModalDisplayed) { + await Promise.all(this.investigations + .filter((investigation) => !investigation.hasEndDate()) + .map(async (investigation) => { + this.clickEndInvestigationItem(investigation); + await nextTick(); + })); + await nextTick(); + this.validateAll({ asTouched: true }); } } } diff --git a/webroot/src/components/PrivilegeCard/PrivilegeCard.vue b/webroot/src/components/PrivilegeCard/PrivilegeCard.vue index 582a9dba5..773283ce6 100644 --- a/webroot/src/components/PrivilegeCard/PrivilegeCard.vue +++ b/webroot/src/components/PrivilegeCard/PrivilegeCard.vue @@ -64,6 +64,26 @@ > {{ $t('licensing.unencumber') }} +
  • + {{ $t('licensing.addInvestigation') }} +
  • +
  • + {{ $t('licensing.endInvestigation') }} +
  • @@ -171,21 +191,25 @@ :isEnabled="isMockPopulateEnabled" @selected="mockPopulate" /> -
    -
    {{ $t('licensing.practitionerName') }}
    -
    {{ licenseeName }}
    -
    -
    -
    {{ $t('common.state') }}
    -
    {{ stateContent }}
    -
    -
    -
    {{ $t('licensing.privilegeId') }}
    -
    {{ privilegeId }}
    +
    +
    +
    {{ $t('licensing.practitionerName') }}
    +
    {{ licenseeName }}
    +
    +
    +
    {{ $t('common.state') }}
    +
    {{ stateContent }}
    +
    -
    -
    {{ $t('licensing.privilegeType') }}
    -
    {{ privilegeTypeAbbrev }}
    +
    +
    +
    {{ $t('licensing.privilegeId') }}
    +
    {{ privilegeId }}
    +
    +
    +
    {{ $t('licensing.privilegeType') }}
    +
    {{ privilegeTypeAbbrev }}
    +
    @@ -385,6 +409,245 @@
    + + + + + +
    diff --git a/webroot/src/locales/en.json b/webroot/src/locales/en.json index cf8eac659..0761bd20c 100644 --- a/webroot/src/locales/en.json +++ b/webroot/src/locales/en.json @@ -687,6 +687,36 @@ "confirmPrivilegeUnencumberSubmit": "Confirm removal(s)", "confirmPrivilegeUnencumberSuccess": "These encumbrances are set to be removed.", "confirmPrivilegeUnencumberSuccessEndDate": "End date", + "addInvestigation": "Add investigation", + "confirmLicenseInvestigationStartTitle": "Confirm Current Significant Investigative Information", + "confirmLicenseInvestigationStartSubtext": "Clicking “Yes” will notify compact member states of a significant investigation ongoing for this practitioner license. Are you sure you want to proceed?", + "confirmLicenseInvestigationStartSubmit": "Yes, send notification", + "confirmLicenseInvestigationStartSuccess": "Member states will be notified of a significant ongoing investigation", + "confirmPrivilegeInvestigationStartTitle": "Confirm Current Significant Investigative Information", + "confirmPrivilegeInvestigationStartSubtext": "Clicking “Yes” will notify compact member states of a significant investigation ongoing for this practitioner privilege. Are you sure you want to proceed?", + "confirmPrivilegeInvestigationStartSubmit": "Yes, send notification", + "confirmPrivilegeInvestigationStartSuccess": "Member states will be notified of a significant ongoing investigation", + "endInvestigation": "End investigation", + "investigationStartedOn": "Investigation started on {date}", + "investigationEndedOn": "Ended on {date}", + "confirmLicenseInvestigationEndSelectTitle": "Select an investigation to end", + "confirmLicenseInvestigationEndTitle": "End an investigation", + "confirmLicenseInvestigationEndSubtext1": "You are ending an investigation that started on {date}.", + "confirmLicenseInvestigationEndSubtext2": "Do you need to report an encumbrance? If so, click “Add encumbrance” below. If not, click “No encumbrance” below, and other states will be notified that the investigation was ended with no encumbrance.", + "confirmLicenseInvestigationEndSubmitWithoutEncumber": "No encumbrance", + "confirmLicenseInvestigationEndSubmitWithEncumber": "Add encumbrance", + "confirmLicenseInvestigationEndSuccess": "This investigation has ended and no encumbrance was added.", + "confirmPrivilegeInvestigationEndSelectTitle": "Select an investigation to end", + "confirmPrivilegeInvestigationEndTitle": "End an investigation", + "confirmPrivilegeInvestigationEndSubtext1": "You are ending an investigation that started on {date}.", + "confirmPrivilegeInvestigationEndSubtext2": "Do you need to report an encumbrance? If so, click “Add encumbrance” below. If not, click “No encumbrance” below, and other states will be notified that the investigation was ended with no encumbrance.", + "confirmPrivilegeInvestigationEndSubmitWithoutEncumber": "No encumbrance", + "confirmPrivilegeInvestigationEndSubmitWithEncumber": "Add encumbrance", + "confirmPrivilegeInvestigationEndSuccess": "This investigation has ended and no encumbrance was added.", + "underInvestigationStatus": "Investigation", + "underInvestigationAlertLocation": "This practitioner is under investigation in {locations}.", + "underInvestigationAlertMultipleLocations": "multiple states", + "underInvestigationAlertStatus": "Privileges can still be used while under investigation.", "expiringIn": "Expiring in", "events": "Events", "expirationTimeExplanation": "Privilege dates use the UTC-4 time zone. This means that privileges will expire at 11:59PM US Eastern Time during Daylight Savings (summer), and at 10:59 PM in the US Eastern time zone during Standard Time (winter).", diff --git a/webroot/src/locales/es.json b/webroot/src/locales/es.json index ddfcce1a4..5c8fdb75d 100644 --- a/webroot/src/locales/es.json +++ b/webroot/src/locales/es.json @@ -671,6 +671,36 @@ "confirmPrivilegeUnencumberSubmit": "Confirmar eliminación(es)", "confirmPrivilegeUnencumberSuccess": "Está previsto que estos gravámenes se eliminen.", "confirmPrivilegeUnencumberSuccessEndDate": "Fecha de finalización", + "addInvestigation": "Añadir investigación", + "confirmLicenseInvestigationStartTitle": "Confirmar la información de investigación significativa actual", + "confirmLicenseInvestigationStartSubtext": "Al hacer clic en “Sí”, se notificará a los estados miembros del acuerdo sobre una investigación importante en curso relacionada con esta licencia profesional. ¿Está seguro de que desea continuar?", + "confirmLicenseInvestigationStartSubmit": "Sí, enviar notificación", + "confirmLicenseInvestigationStartSuccess": "Los Estados miembros serán notificados de una importante investigación en curso.", + "confirmPrivilegeInvestigationStartTitle": "Confirmar la información de investigación significativa actual", + "confirmPrivilegeInvestigationStartSubtext": "Al hacer clic en “Sí”, se notificará a los Estados miembros del pacto que se está llevando a cabo una investigación importante sobre este privilegio profesional. ¿Está seguro de que desea continuar?", + "confirmPrivilegeInvestigationStartSubmit": "Sí, enviar notificación", + "confirmPrivilegeInvestigationStartSuccess": "Los Estados miembros serán notificados de una importante investigación en curso.", + "endInvestigation": "Fin de la investigación", + "investigationStartedOn": "La investigación comenzó el {date}", + "investigationEndedOn": "Finalizó el {date}", + "confirmLicenseInvestigationEndSelectTitle": "Seleccione una investigación para finalizar", + "confirmLicenseInvestigationEndTitle": "Finalizar una investigación", + "confirmLicenseInvestigationEndSubtext1": "Estás dando por finalizada una investigación que comenzó el {date}.", + "confirmLicenseInvestigationEndSubtext2": "¿Necesita reportar algún gravamen? De ser así, haga clic en “Agregar gravamen” a continuación. De lo contrario, haga clic en “Sin gravamen” a continuación, y se notificará a los demás estados que la investigación concluyó sin que se encontrara ningún gravamen.", + "confirmLicenseInvestigationEndSubmitWithoutEncumber": "Sin gravamen", + "confirmLicenseInvestigationEndSubmitWithEncumber": "Agregar gravamen", + "confirmLicenseInvestigationEndSuccess": "Esta investigación ha concluido y no se ha añadido ningún gravamen.", + "confirmPrivilegeInvestigationEndSelectTitle": "Seleccione una investigación para finalizar", + "confirmPrivilegeInvestigationEndTitle": "Finalizar una investigación", + "confirmPrivilegeInvestigationEndSubtext1": "Estás dando por finalizada una investigación que comenzó el {date}.", + "confirmPrivilegeInvestigationEndSubtext2": "¿Necesita reportar algún gravamen? De ser así, haga clic en “Agregar gravamen” a continuación. De lo contrario, haga clic en “Sin gravamen” a continuación, y se notificará a los demás estados que la investigación concluyó sin que se encontrara ningún gravamen.", + "confirmPrivilegeInvestigationEndSubmitWithoutEncumber": "Sin gravamen", + "confirmPrivilegeInvestigationEndSubmitWithEncumber": "Agregar gravamen", + "confirmPrivilegeInvestigationEndSuccess": "Esta investigación ha concluido y no se ha añadido ningún gravamen.", + "underInvestigationStatus": "Investigación", + "underInvestigationAlertLocation": "Este practicante está bajo investigación en {locations}.", + "underInvestigationAlertMultipleLocations": "múltiples estados", + "underInvestigationAlertStatus": "Los privilegios aún se pueden utilizar mientras se está bajo investigación.", "expiringIn": "Expirando en", "events": "Eventos", "expirationTimeExplanation": "Las fechas privilegiadas utilizan la zona horaria UTC-4. Esto significa que los privilegios expirarán a las 11:59 p. m. hora del este de EE. UU. durante el horario de verano (verano) y a las 10:59 p. m. en la zona horaria del este de EE. UU. durante la hora estándar (invierno)", diff --git a/webroot/src/models/Investigation/Investigation.model.spec.ts b/webroot/src/models/Investigation/Investigation.model.spec.ts new file mode 100644 index 000000000..e5baf42dd --- /dev/null +++ b/webroot/src/models/Investigation/Investigation.model.spec.ts @@ -0,0 +1,167 @@ +// +// Investigation.model.spec.ts +// CompactConnect +// +// Created by InspiringApps on 10/28/2025. +// + +import chaiMatchPattern from 'chai-match-pattern'; +import chai from 'chai'; +import { serverDateFormat, displayDateFormat } from '@/app.config'; +import { Investigation, InvestigationSerializer } from '@models/Investigation/Investigation.model'; +import { State } from '@models/State/State.model'; +import i18n from '@/i18n'; +import moment from 'moment'; + +chai.use(chaiMatchPattern); + +const { expect } = chai; + +describe('Investigation model', () => { + before(() => { + const { tm: $tm, t: $t } = i18n.global; + + (window as any).Vue = { + config: { + globalProperties: { + $tm, + $t, + } + } + }; + i18n.global.locale = 'en'; + }); + it('should create an Investigation model with expected defaults', () => { + const investigation = new Investigation(); + + // Test field values + expect(investigation).to.be.an.instanceof(Investigation); + expect(investigation.id).to.equal(null); + expect(investigation.compactType).to.equal(null); + expect(investigation.providerId).to.equal(null); + expect(investigation.state).to.be.an.instanceof(State); + expect(investigation.type).to.equal(null); + expect(investigation.startDate).to.equal(null); + expect(investigation.updateDate).to.equal(null); + expect(investigation.endDate).to.equal(null); + + // Test methods + expect(investigation.startDateDisplay()).to.equal(''); + expect(investigation.updateDateDisplay()).to.equal(''); + expect(investigation.endDateDisplay()).to.equal(''); + expect(investigation.hasEndDate()).to.equal(false); + expect(investigation.isActive()).to.equal(false); + }); + it('should create an Investigation model with specific values', () => { + const data = { + id: 'test-id', + compactType: 'test-compactType', + providerId: 'test-providerId', + state: new State(), + type: 'test-type', + startDate: 'test-startDate', + updateDate: 'test-updateDate', + endDate: 'test-endDate', + }; + const investigation = new Investigation(data); + + // Test field values + expect(investigation).to.be.an.instanceof(Investigation); + expect(investigation.id).to.equal(data.id); + expect(investigation.compactType).to.equal(data.compactType); + expect(investigation.providerId).to.equal(data.providerId); + expect(investigation.state).to.be.an.instanceof(State); + expect(investigation.type).to.equal(data.type); + expect(investigation.startDate).to.equal(data.startDate); + expect(investigation.updateDate).to.equal(data.updateDate); + expect(investigation.endDate).to.equal(data.endDate); + + // Test methods + expect(investigation.startDateDisplay()).to.equal('Invalid date'); + expect(investigation.updateDateDisplay()).to.equal('Invalid date'); + expect(investigation.endDateDisplay()).to.equal('Invalid date'); + expect(investigation.hasEndDate()).to.equal(true); + expect(investigation.isActive()).to.equal(false); + }); + it('should create an Investigation model with specific values (startDate but no endDate)', () => { + const data = { + startDate: moment().format(serverDateFormat), + }; + const investigation = new Investigation(data); + + // Test field values + expect(investigation).to.be.an.instanceof(Investigation); + expect(investigation.startDate).to.equal(data.startDate); + expect(investigation.endDate).to.equal(null); + + // Test methods + expect(investigation.isActive()).to.equal(true); + }); + it('should create an Investigation model with specific values (endDate but no startDate)', () => { + const data = { + endDate: moment().add(1, 'day').format(serverDateFormat), + }; + const investigation = new Investigation(data); + + // Test field values + expect(investigation).to.be.an.instanceof(Investigation); + expect(investigation.startDate).to.equal(null); + expect(investigation.endDate).to.equal(data.endDate); + + // Test methods + expect(investigation.isActive()).to.equal(true); + }); + it('should create an Investigation model with specific values (endDate of today should count as lifted)', () => { + const data = { + startDate: moment().format(serverDateFormat), + endDate: moment().format(serverDateFormat), + }; + const investigation = new Investigation(data); + + // Test field values + expect(investigation).to.be.an.instanceof(Investigation); + expect(investigation.startDate).to.equal(data.startDate); + expect(investigation.endDate).to.equal(data.endDate); + + // Test methods + expect(investigation.isActive()).to.equal(false); + }); + it('should create an Investigation model with specific values through serializer', () => { + const data = { + investigationId: 'test-id', + compact: 'aslp', + providerId: 'test-providerId', + jurisdiction: 'al', + type: 'test-type', + creationDate: moment.utc().format(serverDateFormat), + dateOfUpdate: moment.utc().format(serverDateFormat), + endDate: moment.utc().add(1, 'day').format(serverDateFormat), + }; + const investigation = InvestigationSerializer.fromServer(data); + + // Test field values + expect(investigation).to.be.an.instanceof(Investigation); + expect(investigation.id).to.equal(data.investigationId); + expect(investigation.compactType).to.equal(data.compact); + expect(investigation.providerId).to.equal(data.providerId); + expect(investigation.state).to.be.an.instanceof(State); + expect(investigation.state.name()).to.equal('Alabama'); + expect(investigation.type).to.equal(data.type); + expect(investigation.startDate).to.equal(data.creationDate); + expect(investigation.updateDate).to.equal(data.dateOfUpdate); + expect(investigation.endDate).to.equal(data.endDate); + + // Test methods + expect(investigation.startDateDisplay()).to.equal( + moment(data.creationDate, serverDateFormat).format(displayDateFormat) + ); + expect(investigation.updateDateDisplay()).to.equal( + moment(data.dateOfUpdate, serverDateFormat).format(displayDateFormat) + ); + expect(investigation.endDateDisplay()).to.equal( + moment(data.endDate, serverDateFormat).format(displayDateFormat) + ); + expect(investigation.hasEndDate()).to.equal(true); + expect(investigation.isActive()).to.equal(true); + }); +}); diff --git a/webroot/src/models/Investigation/Investigation.model.ts b/webroot/src/models/Investigation/Investigation.model.ts new file mode 100644 index 000000000..5c60b8a81 --- /dev/null +++ b/webroot/src/models/Investigation/Investigation.model.ts @@ -0,0 +1,113 @@ +// +// Investigation.ts +// CompactConnect +// +// Created by InspiringApps on 10/28/2025. +// + +import { deleteUndefinedProperties } from '@models/_helpers'; +import { serverDateFormat } from '@/app.config'; +import { dateDisplay } from '@models/_formatters/date'; +import { CompactType } from '@models/Compact/Compact.model'; +import { State } from '@models/State/State.model'; +import moment from 'moment'; +import { StatsigClient } from '@statsig/js-client'; + +// ======================================================== +// = Interface = +// ======================================================== +export interface InterfaceInvestigationCreate { + id?: string | null; + compactType?: CompactType | null; + providerId?: string | null; + state?: State; + type?: string | null; + startDate?: string | null; + updateDate?: string | null; + endDate?: string | null; +} + +// ======================================================== +// = Model = +// ======================================================== +export class Investigation implements InterfaceInvestigationCreate { + public $tm?: any = () => []; + public $features?: StatsigClient | null = null; + public id? = null; + public compactType? = null; + public providerId? = null; + public state? = new State(); + public type? = null; + public startDate? = null; + public updateDate? = null; + public endDate? = null; + + constructor(data?: InterfaceInvestigationCreate) { + const cleanDataObject = deleteUndefinedProperties(data); + const global = window as any; + const { $tm, $features } = global.Vue?.config?.globalProperties || {}; + + this.$tm = $tm; + this.$features = $features; + + Object.assign(this, cleanDataObject); + } + + // Helper methods + public startDateDisplay(): string { + return dateDisplay(this.startDate); + } + + public updateDateDisplay(): string { + return dateDisplay(this.updateDate); + } + + public endDateDisplay(): string { + return dateDisplay(this.endDate); + } + + public hasEndDate(): boolean { + return Boolean(this.endDate); + } + + public isActive(): boolean { + // Determine whether the investigation is currently in effect + const { startDate, endDate } = this; + const startDateMoment = (startDate) ? moment(startDate, serverDateFormat) : null; + const endDateMoment = (endDate) ? moment(endDate, serverDateFormat) : null; + const now = moment(); + const isAfterStartDate = (startDateMoment?.isValid()) ? now.isSameOrAfter(startDateMoment, 'day') : false; + const isBeforeEndDate = (endDateMoment?.isValid()) ? now.isBefore(endDateMoment, 'day') : false; + let isInvestigationActive = false; + + if (isAfterStartDate && isBeforeEndDate) { + isInvestigationActive = true; + } else if (startDate && !endDate && isAfterStartDate) { + isInvestigationActive = true; + } else if (endDate && !startDate && isBeforeEndDate) { + isInvestigationActive = true; + } + + return isInvestigationActive; + } +} + +// ======================================================== +// = Serializer = +// ======================================================== +export class InvestigationSerializer { + static fromServer(json: any): Investigation { + const investigationData = { + id: json.investigationId, + compactType: json.compact, + providerId: json.providerId, + state: new State({ abbrev: json.jurisdiction }), + type: json.type, + startDate: json.creationDate, + updateDate: json.dateOfUpdate, + endDate: json.endDate, + }; + + return new Investigation(investigationData); + } +} diff --git a/webroot/src/models/License/License.model.spec.ts b/webroot/src/models/License/License.model.spec.ts index ccc47cba6..611090012 100644 --- a/webroot/src/models/License/License.model.spec.ts +++ b/webroot/src/models/License/License.model.spec.ts @@ -19,6 +19,7 @@ import { State } from '@models/State/State.model'; import { Address } from '@models/Address/Address.model'; import { LicenseHistoryItem } from '@models/LicenseHistoryItem/LicenseHistoryItem.model'; import { AdverseAction } from '@models/AdverseAction/AdverseAction.model'; +import { Investigation } from '@models/Investigation/Investigation.model'; import i18n from '@/i18n'; import moment from 'moment'; @@ -64,6 +65,7 @@ describe('License model', () => { expect(license.statusDescription).to.equal(null); expect(license.eligibility).to.equal(EligibilityStatus.INELIGIBLE); expect(license.adverseActions).to.matchPattern([]); + expect(license.investigations).to.matchPattern([]); // Test methods expect(license.issueDateDisplay()).to.equal(''); @@ -77,6 +79,7 @@ describe('License model', () => { expect(license.displayName()).to.equal('Unknown'); expect(license.isEncumbered()).to.equal(false); expect(license.isLatestLiftedEncumbranceWithinWaitPeriod()).to.equal(false); + expect(license.isUnderInvestigation()).to.equal(false); }); it('should create a License with specific values', () => { const data = { @@ -99,6 +102,7 @@ describe('License model', () => { statusDescription: 'test-status-desc', eligibility: EligibilityStatus.ELIGIBLE, adverseActions: [new AdverseAction()], + investigations: [new Investigation()], }; const license = new License(data); @@ -123,6 +127,7 @@ describe('License model', () => { expect(license.statusDescription).to.equal(data.statusDescription); expect(license.eligibility).to.equal(data.eligibility); expect(license.adverseActions[0]).to.be.an.instanceof(AdverseAction); + expect(license.investigations[0]).to.be.an.instanceof(Investigation); // Test methods expect(license.issueDateDisplay()).to.equal('Invalid date'); @@ -137,6 +142,7 @@ describe('License model', () => { expect(license.displayName(', ', true)).to.equal('Unknown, AUD'); expect(license.isEncumbered()).to.equal(false); expect(license.isLatestLiftedEncumbranceWithinWaitPeriod()).to.equal(false); + expect(license.isUnderInvestigation()).to.equal(false); }); it('should create a License with specific values (custom displayName delimiter)', () => { const data = { @@ -181,6 +187,17 @@ describe('License model', () => { effectiveLiftDate: moment().add(1, 'day').format(serverDateFormat), }, ], + investigations: [ + { + investigationId: 'test-id', + compact: CompactType.ASLP, + providerId: 'test-provider-id', + jurisdiction: 'al', + type: 'investigation', + creationDate: moment().subtract(1, 'day').format(serverDateFormat), + dateOfUpdate: moment().add(1, 'day').format(serverDateFormat), + }, + ], }; const license = LicenseSerializer.fromServer(data); @@ -203,6 +220,8 @@ describe('License model', () => { expect(license.eligibility).to.equal(data.compactEligibility); expect(license.adverseActions).to.be.an('array').with.length(1); expect(license.adverseActions[0]).to.be.an.instanceof(AdverseAction); + expect(license.investigations).to.be.an('array').with.length(1); + expect(license.investigations[0]).to.be.an.instanceof(Investigation); // Test methods expect(license.issueDateDisplay()).to.equal( @@ -222,6 +241,7 @@ describe('License model', () => { expect(license.licenseTypeAbbreviation()).to.equal('AUD'); expect(license.isEncumbered()).to.equal(true); expect(license.isLatestLiftedEncumbranceWithinWaitPeriod()).to.equal(false); + expect(license.isUnderInvestigation()).to.equal(true); }); it('should create a privilege with specific values through serializer', () => { const data = { @@ -244,6 +264,17 @@ describe('License model', () => { effectiveLiftDate: moment().subtract(3, 'months').format(serverDateFormat), }, ], + investigations: [ + { + investigationId: 'test-id', + compact: CompactType.ASLP, + providerId: 'test-provider-id', + jurisdiction: 'al', + type: 'investigation', + creationDate: moment().subtract(1, 'day').format(serverDateFormat), + dateOfUpdate: moment().add(1, 'day').format(serverDateFormat), + }, + ], attestations: [ { attestationId: 'personal-information-address-attestation', @@ -550,6 +581,8 @@ describe('License model', () => { expect(license.adverseActions).to.be.an('array').with.length(1); expect(license.adverseActions[0]).to.be.an.instanceof(AdverseAction); expect(license.adverseActions[0].endDate).to.equal(data.adverseActions[0].effectiveLiftDate); + expect(license.investigations).to.be.an('array').with.length(1); + expect(license.investigations[0]).to.be.an.instanceof(Investigation); // Test methods expect(license.issueDateDisplay()).to.equal( @@ -573,6 +606,7 @@ describe('License model', () => { expect(license.history.length).to.equal(0); expect(license.isEncumbered()).to.equal(false); expect(license.isLatestLiftedEncumbranceWithinWaitPeriod()).to.equal(true); + expect(license.isUnderInvestigation()).to.equal(true); }); it('should populate isDeactivated correctly given license history (deactivation)', () => { const data = { diff --git a/webroot/src/models/License/License.model.ts b/webroot/src/models/License/License.model.ts index 9093e8ffb..318611453 100644 --- a/webroot/src/models/License/License.model.ts +++ b/webroot/src/models/License/License.model.ts @@ -13,6 +13,7 @@ import { State } from '@models/State/State.model'; import { LicenseHistoryItem } from '@models/LicenseHistoryItem/LicenseHistoryItem.model'; import { Address, AddressSerializer } from '@models/Address/Address.model'; import { AdverseAction, AdverseActionSerializer } from '@models/AdverseAction/AdverseAction.model'; +import { Investigation, InvestigationSerializer } from '@models/Investigation/Investigation.model'; import moment from 'moment'; import { StatsigClient } from '@statsig/js-client'; @@ -63,6 +64,7 @@ export interface InterfaceLicense { statusDescription?: string | null, eligibility?: EligibilityStatus, adverseActions?: Array, + investigations?: Array, } // ======================================================== @@ -91,6 +93,7 @@ export class License implements InterfaceLicense { public statusDescription? = null; public eligibility? = EligibilityStatus.INELIGIBLE; public adverseActions? = []; + public investigations? = []; constructor(data?: InterfaceLicense) { const cleanDataObject = deleteUndefinedProperties(data); @@ -183,6 +186,10 @@ export class License implements InterfaceLicense { return isWithinWaitPeriod; } + + public isUnderInvestigation(): boolean { + return this.investigations?.some((investigation: Investigation) => investigation.isActive()) || false; + } } // ======================================================== @@ -217,6 +224,7 @@ export class LicenseSerializer { ? json.compactEligibility : EligibilityStatus.NA, adverseActions: [] as Array, + investigations: [] as Array, }; if (Array.isArray(json.adverseActions)) { @@ -225,6 +233,12 @@ export class LicenseSerializer { }); } + if (Array.isArray(json.investigations)) { + json.investigations.forEach((serverInvestigation) => { + licenseData.investigations.push(InvestigationSerializer.fromServer(serverInvestigation)); + }); + } + return new License(licenseData); } } diff --git a/webroot/src/models/Licensee/Licensee.model.spec.ts b/webroot/src/models/Licensee/Licensee.model.spec.ts index 146c374cc..71b5c8f93 100644 --- a/webroot/src/models/Licensee/Licensee.model.spec.ts +++ b/webroot/src/models/Licensee/Licensee.model.spec.ts @@ -18,6 +18,7 @@ import { EligibilityStatus } from '@models/License/License.model'; import { MilitaryAffiliation } from '@models/MilitaryAffiliation/MilitaryAffiliation.model'; +import { Investigation } from '@models/Investigation/Investigation.model'; import { State } from '@models/State/State.model'; import i18n from '@/i18n'; import moment from 'moment'; @@ -90,6 +91,10 @@ describe('Licensee model', () => { expect(licensee.hasEncumberedPrivileges()).to.equal(false); expect(licensee.isEncumbered()).to.equal(false); expect(licensee.hasEncumbranceLiftedWithinWaitPeriod()).to.equal(false); + expect(licensee.hasUnderInvestigationLicenses()).to.equal(false); + expect(licensee.hasUnderInvestigationPrivileges()).to.equal(false); + expect(licensee.isUnderInvestigation()).to.equal(false); + expect(licensee.underInvestigationStates()).to.matchPattern([]); }); it('should create a Licensee with specific values', () => { const data = { @@ -198,6 +203,10 @@ describe('Licensee model', () => { expect(licensee.hasEncumberedPrivileges()).to.equal(false); expect(licensee.isEncumbered()).to.equal(false); expect(licensee.hasEncumbranceLiftedWithinWaitPeriod()).to.equal(false); + expect(licensee.hasUnderInvestigationLicenses()).to.equal(false); + expect(licensee.hasUnderInvestigationPrivileges()).to.equal(false); + expect(licensee.isUnderInvestigation()).to.equal(false); + expect(licensee.underInvestigationStates()).to.matchPattern([]); }); it('should create a Licensee with specific values (empty state name fallbacks)', () => { const licensee = new Licensee(); @@ -497,6 +506,10 @@ describe('Licensee model', () => { expect(licensee.hasEncumberedPrivileges()).to.equal(false); expect(licensee.isEncumbered()).to.equal(false); expect(licensee.hasEncumbranceLiftedWithinWaitPeriod()).to.equal(false); + expect(licensee.hasUnderInvestigationLicenses()).to.equal(false); + expect(licensee.hasUnderInvestigationPrivileges()).to.equal(false); + expect(licensee.isUnderInvestigation()).to.equal(false); + expect(licensee.underInvestigationStates()).to.matchPattern([]); }); it('should create a Licensee with specific values through serializer (with inactive best license)', () => { const data = { @@ -631,6 +644,10 @@ describe('Licensee model', () => { expect(licensee.hasEncumberedPrivileges()).to.equal(false); expect(licensee.isEncumbered()).to.equal(false); expect(licensee.hasEncumbranceLiftedWithinWaitPeriod()).to.equal(false); + expect(licensee.hasUnderInvestigationLicenses()).to.equal(false); + expect(licensee.hasUnderInvestigationPrivileges()).to.equal(false); + expect(licensee.isUnderInvestigation()).to.equal(false); + expect(licensee.underInvestigationStates()).to.matchPattern([]); }); it('should create a Licensee with specific values through serializer (with initiliazing military status)', () => { const data = { @@ -1111,6 +1128,50 @@ describe('Licensee model', () => { expect(licensee.hasEncumberedPrivileges()).to.equal(true); expect(licensee.isEncumbered()).to.equal(true); }); + it('should create a Licensee with under-investigation licenses and privileges', () => { + const homeState = new State({ abbrev: 'co' }); + const underInvestigationLicense = new License({ + issueState: homeState, + licenseNumber: 'investigation-license', + status: LicenseStatus.ACTIVE, + eligibility: EligibilityStatus.ELIGIBLE, + investigations: [new Investigation({ + state: new State({ abbrev: 'al' }), + startDate: moment().subtract(1, 'day').format(serverDateFormat), + updateDate: moment().add(1, 'day').format(serverDateFormat), + })], + }); + const underInvestigationPrivilege = new License({ + licenseNumber: 'investigation-privilege', + investigations: [ + new Investigation({ + state: new State({ abbrev: 'al' }), + startDate: moment().subtract(1, 'day').format(serverDateFormat), + updateDate: moment().add(1, 'day').format(serverDateFormat), + }), + new Investigation({ + state: new State({ abbrev: 'co' }), + startDate: moment().subtract(1, 'day').format(serverDateFormat), + updateDate: moment().add(1, 'day').format(serverDateFormat), + }), + ], + }); + const licensee = new Licensee({ + homeJurisdiction: homeState, + licenses: [underInvestigationLicense], + privileges: [underInvestigationPrivilege], + }); + + // Test encumbered methods + expect(licensee.hasUnderInvestigationLicenses()).to.equal(true); + expect(licensee.hasUnderInvestigationPrivileges()).to.equal(true); + expect(licensee.isUnderInvestigation()).to.equal(true); + expect(licensee.underInvestigationStates()).to.matchPattern([ + new State({ abbrev: 'al' }), + new State({ abbrev: 'co' }), + ]); + expect(licensee.canPurchasePrivileges()).to.equal(true); + }); it(`should handle 'unknown' currentHomeJurisdiction by falling back to licenseJurisdiction`, () => { const data = { providerId: 'test-id', diff --git a/webroot/src/models/Licensee/Licensee.model.ts b/webroot/src/models/Licensee/Licensee.model.ts index 5eb8c8f2a..a15355592 100644 --- a/webroot/src/models/Licensee/Licensee.model.ts +++ b/webroot/src/models/Licensee/Licensee.model.ts @@ -17,6 +17,7 @@ import { EligibilityStatus } from '@models/License/License.model'; import { MilitaryAffiliation, MilitaryAffiliationSerializer } from '@models/MilitaryAffiliation/MilitaryAffiliation.model'; +import { Investigation } from '@models/Investigation/Investigation.model'; import { State } from '@models/State/State.model'; import moment from 'moment'; import { StatsigClient } from '@statsig/js-client'; @@ -329,6 +330,59 @@ export class Licensee implements InterfaceLicensee { privilege.isLatestLiftedEncumbranceWithinWaitPeriod()) || false; } + public hasUnderInvestigationLicenses(): boolean { + return this.licenses?.some((license: License) => license.isUnderInvestigation()) || false; + } + + public hasUnderInvestigationPrivileges(): boolean { + return this.privileges?.some((privilege: License) => privilege.isUnderInvestigation()) || false; + } + + public isUnderInvestigation(): boolean { + return this.hasUnderInvestigationLicenses() || this.hasUnderInvestigationPrivileges(); + } + + public underInvestigationStates(): Array { + const investigationStates: Array = []; + const investigationStatesAbbrev: Array = []; + + this.licenses?.forEach((license: License) => { + if (license.isUnderInvestigation()) { + license.investigations?.forEach((investigation: Investigation) => { + const investigationState = investigation.state; + const investigationStateAbbrev = investigation.state?.abbrev; + + if (investigation.isActive() + && investigationState + && investigationStateAbbrev + && !investigationStatesAbbrev.includes(investigationStateAbbrev)) { + investigationStates.push(investigationState); + investigationStatesAbbrev.push(investigationStateAbbrev); + } + }); + } + }); + + this.privileges?.forEach((privilege: License) => { + if (privilege.isUnderInvestigation()) { + privilege.investigations?.forEach((investigation: Investigation) => { + const investigationState = investigation.state; + const investigationStateAbbrev = investigation.state?.abbrev; + + if (investigation.isActive() + && investigationState + && investigationStateAbbrev + && !investigationStatesAbbrev.includes(investigationStateAbbrev)) { + investigationStates.push(investigationState); + investigationStatesAbbrev.push(investigationStateAbbrev); + } + }); + } + }); + + return investigationStates; + } + public purchaseEligibleLicenses(): Array { return this.activeHomeJurisdictionLicenses() .filter((license: License) => (license.eligibility === EligibilityStatus.ELIGIBLE)); diff --git a/webroot/src/network/data.api.ts b/webroot/src/network/data.api.ts index cdde3cb5a..0b1d640eb 100644 --- a/webroot/src/network/data.api.ts +++ b/webroot/src/network/data.api.ts @@ -212,6 +212,49 @@ export class DataApi { ); } + /** + * POST Create License Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} licenseState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @return {Promise} The server response. + */ + public createLicenseInvestigation( + compact, + licenseeId, + licenseState, + licenseType + ) { + return licenseDataApi.createLicenseInvestigation( + compact, + licenseeId, + licenseState, + licenseType + ); + } + + /** + * Update License Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} licenseState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @param {string} investigationId The Investigation ID. + * @param {object} [encumbrance] Optional encumbrance config to add to the privilege. + * @return {Promise} The server response. + */ + public updateLicenseInvestigation(compact, licenseeId, licenseState, licenseType, investigationId, encumbrance) { + return licenseDataApi.updateLicenseInvestigation( + compact, + licenseeId, + licenseState, + licenseType, + investigationId, + encumbrance + ); + } + /** * DELETE Privilege for a licensee. * @param {string} compact The compact string ID (aslp, octp, coun). @@ -280,6 +323,56 @@ export class DataApi { ); } + /** + * POST Create Privilege Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} privilegeState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @return {Promise} The server response. + */ + public createPrivilegeInvestigation( + compact, + licenseeId, + privilegeState, + licenseType + ) { + return licenseDataApi.createPrivilegeInvestigation( + compact, + licenseeId, + privilegeState, + licenseType + ); + } + + /** + * Update Privilege Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} privilegeState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @param {string} investigationId The Investigation ID. + * @param {object} [encumbrance] Optional encumbrance config to add to the privilege. + * @return {Promise} The server response. + */ + public updatePrivilegeInvestigation( + compact, + licenseeId, + privilegeState, + licenseType, + investigationId, + encumbrance + ) { + return licenseDataApi.updatePrivilegeInvestigation( + compact, + licenseeId, + privilegeState, + licenseType, + investigationId, + encumbrance + ); + } + /** * GET Licensee SSN by ID. * @param {string} compact A compact type. diff --git a/webroot/src/network/licenseApi/data.api.ts b/webroot/src/network/licenseApi/data.api.ts index 6dfc3b2bd..2f52256a3 100644 --- a/webroot/src/network/licenseApi/data.api.ts +++ b/webroot/src/network/licenseApi/data.api.ts @@ -316,6 +316,77 @@ export class LicenseDataApi implements DataApiInterface { return serverResponse; } + /** + * POST Create License Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} licenseState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @return {Promise} The server response. + */ + public async createLicenseInvestigation( + compact: string, + licenseeId: string, + licenseState: string, + licenseType: string + ) { + const serverResponse: any = await this.api.post(`/v1/compacts/${compact}/providers/${licenseeId}/licenses/jurisdiction/${licenseState}/licenseType/${licenseType}/investigation`, {}); + + return serverResponse; + } + + /** + * PATCH Update License Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} licenseState The 2-character state abbreviation for the License. + * @param {string} licenseType The license type. + * @param {string} investigationId The Investigation ID. + * @param {object} [encumbrance] Optional encumbrance config to add to the license. + * @param {string} encumbranceType The discipline action type. + * @param {string} npdbCategory The NPDB category name. + * @param {Array} npdbCategories The NPDB category list. + * @param {string} startDate The encumber start date. + * @return {Promise} The server response. + */ + public async updateLicenseInvestigation( + compact: string, + licenseeId: string, + licenseState: string, + licenseType: string, + investigationId: string, + encumbrance?: { + encumbranceType: string, + npdbCategory: string, + npdbCategories: Array, + startDate: string + } + ) { + const { $features } = (window as any).Vue?.config?.globalProperties || {}; + const serverResponse: any = await this.api.patch(`/v1/compacts/${compact}/providers/${licenseeId}/licenses/jurisdiction/${licenseState}/licenseType/${licenseType}/investigation/${investigationId}`, { + action: 'close', + ...(encumbrance + ? { + encumbrance: { + encumbranceType: encumbrance.encumbranceType, + ...($features?.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + clinicalPrivilegeActionCategories: encumbrance.npdbCategories, + } + : { + clinicalPrivilegeActionCategory: encumbrance.npdbCategory, + } + ), + encumbranceEffectiveDate: encumbrance.startDate, + }, + } + : {} + ), + }); + + return serverResponse; + } + /** * DELETE Privilege for a licensee. * @param {string} compact The compact string ID (aslp, octp, coun). @@ -403,6 +474,77 @@ export class LicenseDataApi implements DataApiInterface { return serverResponse; } + /** + * POST Create Privilege Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} privilegeState The 2-character state abbreviation for the Privilege. + * @param {string} licenseType The license type. + * @return {Promise} The server response. + */ + public async createPrivilegeInvestigation( + compact: string, + licenseeId: string, + privilegeState: string, + licenseType: string + ) { + const serverResponse: any = await this.api.post(`/v1/compacts/${compact}/providers/${licenseeId}/privileges/jurisdiction/${privilegeState}/licenseType/${licenseType}/investigation`, {}); + + return serverResponse; + } + + /** + * PATCH Update Privilege Investigation for a licensee. + * @param {string} compact The compact string ID (aslp, octp, coun). + * @param {string} licenseeId The Licensee ID. + * @param {string} privilegeState The 2-character state abbreviation for the Privilege. + * @param {string} licenseType The license type. + * @param {string} investigationId The Investigation ID. + * @param {object} [encumbrance] Optional encumbrance config to add to the privilege. + * @param {string} encumbranceType The discipline action type. + * @param {string} npdbCategory The NPDB category name. + * @param {Array} npdbCategories The NPDB category list. + * @param {string} startDate The encumber start date. + * @return {Promise} The server response. + */ + public async updatePrivilegeInvestigation( + compact: string, + licenseeId: string, + privilegeState: string, + licenseType: string, + investigationId: string, + encumbrance?: { + encumbranceType: string, + npdbCategory: string, + npdbCategories: Array, + startDate: string + } + ) { + const { $features } = (window as any).Vue?.config?.globalProperties || {}; + const serverResponse: any = await this.api.patch(`/v1/compacts/${compact}/providers/${licenseeId}/privileges/jurisdiction/${privilegeState}/licenseType/${licenseType}/investigation/${investigationId}`, { + action: 'close', + ...(encumbrance + ? { + encumbrance: { + encumbranceType: encumbrance.encumbranceType, + ...($features?.checkGate(FeatureGates.ENCUMBER_MULTI_CATEGORY) + ? { + clinicalPrivilegeActionCategories: encumbrance.npdbCategories, + } + : { + clinicalPrivilegeActionCategory: encumbrance.npdbCategory, + } + ), + encumbranceEffectiveDate: encumbrance.startDate, + }, + } + : {} + ), + }); + + return serverResponse; + } + /** * GET SSN for licensee by ID. * @param {string} licenseeId A licensee ID. diff --git a/webroot/src/network/mocks/mock.data.api.ts b/webroot/src/network/mocks/mock.data.api.ts index d4e0c3fb2..5956658ec 100644 --- a/webroot/src/network/mocks/mock.data.api.ts +++ b/webroot/src/network/mocks/mock.data.api.ts @@ -278,6 +278,43 @@ export class DataApi { })); } + // Create License Investigation for a licensee. + public createLicenseInvestigation( + compact, + licenseeId, + licenseState, + licenseType + ) { + if (!compact) { + return Promise.reject(new Error('failed license investigation create')); + } + + return wait(500).then(() => ({ + message: 'success', + compact, + licenseeId, + licenseState, + licenseType, + })); + } + + // Update License Investigation for a licensee. + public updateLicenseInvestigation(compact, licenseeId, licenseState, licenseType, investigationId, encumbrance) { + if (!compact) { + return Promise.reject(new Error('failed license investigation update')); + } + + return wait(500).then(() => ({ + message: 'success', + compact, + licenseeId, + licenseState, + licenseType, + investigationId, + encumbrance, + })); + } + // Delete Privilege for a licensee. public deletePrivilege(compact, licenseeId, privilegeState, licenseType) { if (!compact) { @@ -346,6 +383,50 @@ export class DataApi { })); } + // Create Privilege Investigation for a licensee. + public createPrivilegeInvestigation( + compact, + licenseeId, + privilegeState, + licenseType + ) { + if (!compact) { + return Promise.reject(new Error('failed privilege investigation create')); + } + + return wait(500).then(() => ({ + message: 'success', + compact, + licenseeId, + privilegeState, + licenseType, + })); + } + + // Update Privilege Investigation for a licensee. + public updatePrivilegeInvestigation( + compact, + licenseeId, + privilegeState, + licenseType, + investigationId, + encumbrance + ) { + if (!compact) { + return Promise.reject(new Error('failed privilege investigation update')); + } + + return wait(500).then(() => ({ + message: 'success', + compact, + licenseeId, + privilegeState, + licenseType, + investigationId, + encumbrance, + })); + } + // Get full SSN for licensee public getLicenseeSsn(compact, licenseeId) { return wait(500).then(() => ({ diff --git a/webroot/src/network/mocks/mock.data.ts b/webroot/src/network/mocks/mock.data.ts index a3d44692a..2c37f3251 100644 --- a/webroot/src/network/mocks/mock.data.ts +++ b/webroot/src/network/mocks/mock.data.ts @@ -85,6 +85,14 @@ export const staffAccount = { readSsn: true, }, }, + wy: { + actions: { + admin: true, + write: true, + readPrivate: true, + readSsn: true, + }, + }, }, }, aslp: { @@ -761,6 +769,30 @@ export const licensees = { liftingUser: null, }, ], + investigations: [ + { + investigationId: '12345-ABC', + providerId: 'aa2e057d-6972-4a68-a55d-aad1c3d05278', + compact: 'octp', + jurisdiction: 'wy', + licenseType: 'occupational therapy assistant', + type: 'investigation', + creationDate: moment().subtract(1, 'week').format(serverDatetimeFormat), + dateOfUpdate: moment().subtract(1, 'week').format(serverDatetimeFormat), + endDate: null, + }, + { + investigationId: '12345-DEF', + providerId: 'aa2e057d-6972-4a68-a55d-aad1c3d05278', + compact: 'octp', + jurisdiction: 'wy', + licenseType: 'occupational therapy assistant', + type: 'investigation', + creationDate: moment().subtract(1, 'month').format(serverDatetimeFormat), + dateOfUpdate: moment().subtract(1, 'month').format(serverDatetimeFormat), + endDate: moment().subtract(2, 'weeks').format(serverDatetimeFormat), + }, + ], }, ], privilegeJurisdictions: [ @@ -911,6 +943,58 @@ export const licensees = { }, ], }, + { + privilegeId: 'OTA-WY-1', + providerId: 'aa2e057d-6972-4a68-a55d-aad1c3d05278', + compact: 'octp', + compactTransactionId: '120060086502', + type: 'privilege', + jurisdiction: 'wy', + licenseJurisdiction: 'co', + licenseType: 'occupational therapy assistant', + persistedStatus: 'active', + status: 'active', + dateOfIssuance: '2024-03-19T21:30:27+00:00', + dateOfUpdate: '2025-03-26T15:56:58+00:00', + dateOfRenewal: moment().subtract(11, 'months').format(serverDateFormat), + dateOfExpiration: moment().add(1, 'month').format(serverDateFormat), + attestations: attestationResponses.map((response) => ({ ...response })), + investigations: [ + { + investigationId: '12345-ABC', + providerId: 'aa2e057d-6972-4a68-a55d-aad1c3d05278', + compact: 'octp', + jurisdiction: 'wy', + licenseType: 'occupational therapy assistant', + type: 'investigation', + creationDate: moment().subtract(1, 'week').format(serverDatetimeFormat), + dateOfUpdate: null, + endDate: null, + }, + { + investigationId: '12345-DEF', + providerId: 'aa2e057d-6972-4a68-a55d-aad1c3d05278', + compact: 'octp', + jurisdiction: 'wy', + licenseType: 'occupational therapy assistant', + type: 'investigation', + creationDate: moment().subtract(1, 'month').format(serverDatetimeFormat), + dateOfUpdate: moment().subtract(1, 'month').format(serverDatetimeFormat), + endDate: moment().subtract(3, 'weeks').format(serverDatetimeFormat), + }, + { + investigationId: '12345-GHI', + providerId: 'aa2e057d-6972-4a68-a55d-aad1c3d05278', + compact: 'octp', + jurisdiction: 'wy', + licenseType: 'occupational therapy assistant', + type: 'investigation', + creationDate: moment().subtract(1, 'year').format(serverDatetimeFormat), + dateOfUpdate: null, + endDate: null, + }, + ], + }, ], }, { @@ -1836,6 +1920,33 @@ export const mockPrivilegeHistoryResponses = [ } ] }, + { + // ================================================================ + // JANET DOE (WY OTA) + // ================================================================ + providerId: 'aa2e057d-6972-4a68-a55d-aad1c3d05278', + compact: 'octp', + jurisdiction: 'wy', + licenseType: 'occupational therapy assistant', + privilegeId: 'OTA-WY-1', + events: [ + { + type: 'privilegeUpdate', + updateType: 'issuance', + dateOfUpdate: '2024-03-19T21:30:27+00:00', + effectiveDate: '2024-03-19T21:30:27+00:00', + createDate: '2024-03-19T21:30:27+00:00' + }, + { + type: 'privilegeUpdate', + updateType: 'investigation', + dateOfUpdate: moment().subtract(1, 'week').format(serverDatetimeFormat), + effectiveDate: moment().subtract(1, 'week').format(serverDatetimeFormat), + createDate: moment().subtract(1, 'week').format(serverDatetimeFormat), + note: '', + } + ] + }, { // ================================================================ // TYLER DURDEN (AL OTA) diff --git a/webroot/src/pages/LicensingDetail/LicensingDetail.less b/webroot/src/pages/LicensingDetail/LicensingDetail.less index a08d179bd..9b92daf25 100644 --- a/webroot/src/pages/LicensingDetail/LicensingDetail.less +++ b/webroot/src/pages/LicensingDetail/LicensingDetail.less @@ -10,6 +10,22 @@ @spacingTablet: 4.8rem; @spacingDesktop: 12rem; + .licensee-alert { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + padding: 0.8rem 2rem; + font-weight: @fontWeightBold; + background-color: @midYellow; + + .alert-icon { + height: 1.8rem; + margin-right: 0.8rem; + stroke: @fontColor; + } + } + .title-row { display: flex; flex-direction: row; diff --git a/webroot/src/pages/LicensingDetail/LicensingDetail.ts b/webroot/src/pages/LicensingDetail/LicensingDetail.ts index 156c9985d..c486f7a40 100644 --- a/webroot/src/pages/LicensingDetail/LicensingDetail.ts +++ b/webroot/src/pages/LicensingDetail/LicensingDetail.ts @@ -12,6 +12,7 @@ import LicenseCard from '@/components/LicenseCard/LicenseCard.vue'; import PrivilegeCard from '@/components/PrivilegeCard/PrivilegeCard.vue'; import MilitaryAffiliationInfoBlock from '@components/MilitaryAffiliationInfoBlock/MilitaryAffiliationInfoBlock.vue'; import CollapseCaretButton from '@components/CollapseCaretButton/CollapseCaretButton.vue'; +import AlertIcon from '@components/Icons/AlertTriangle/AlertTriangle.vue'; import LicenseIcon from '@components/Icons/LicenseIcon/LicenseIcon.vue'; import ExpirationExplanationIcon from '@components/Icons/ExpirationExplanationIcon/ExpirationExplanationIcon.vue'; import { CompactType } from '@models/Compact/Compact.model'; @@ -28,6 +29,7 @@ import { dataApi } from '@network/data.api'; LicenseCard, PrivilegeCard, CollapseCaretButton, + AlertIcon, LicenseIcon, MilitaryAffiliationInfoBlock, ExpirationExplanationIcon @@ -106,6 +108,25 @@ export default class LicensingDetail extends Vue { return storeRecord; } + get isLicenseeUnderInvestigation(): boolean { + return this.licensee?.isUnderInvestigation() || false; + } + + get licenseeInvestigationAlertContent(): string { + const investigationStates = this.licensee?.underInvestigationStates() || []; + const statesContent = (investigationStates.length === 1) + ? investigationStates[0].name() + : this.$t('licensing.underInvestigationAlertMultipleLocations'); + let alertContent = ''; + + if (investigationStates.length) { + alertContent += `${this.$t('licensing.underInvestigationAlertLocation', { locations: statesContent })} + ${this.$t('licensing.underInvestigationAlertStatus')}`; + } + + return alertContent; + } + get licenseeNameDisplay(): string { return this.licensee?.nameDisplay() || ''; } diff --git a/webroot/src/pages/LicensingDetail/LicensingDetail.vue b/webroot/src/pages/LicensingDetail/LicensingDetail.vue index 04f5ae847..1d401b90a 100644 --- a/webroot/src/pages/LicensingDetail/LicensingDetail.vue +++ b/webroot/src/pages/LicensingDetail/LicensingDetail.vue @@ -11,6 +11,10 @@
    +
    + + {{ licenseeInvestigationAlertContent }} +