From ece7469832f7ab2fcdcbc079783f1b47003342f7 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 5 Aug 2026 10:33:52 -0500 Subject: [PATCH 1/4] Add check for missing single-state license during ingest --- .../provider-data-v1/handlers/ingest.py | 95 +++++++-- .../function/test_handlers/test_ingest.py | 191 ++++++++++++++++-- 2 files changed, 256 insertions(+), 30 deletions(-) diff --git a/backend/social-work-app/lambdas/python/provider-data-v1/handlers/ingest.py b/backend/social-work-app/lambdas/python/provider-data-v1/handlers/ingest.py index fd0072822..2b49a8805 100644 --- a/backend/social-work-app/lambdas/python/provider-data-v1/handlers/ingest.py +++ b/backend/social-work-app/lambdas/python/provider-data-v1/handlers/ingest.py @@ -50,6 +50,12 @@ 'in the same jurisdiction is ineligible.' ) +MULTI_STATE_MISSING_SINGLE_STATE_MESSAGE = ( + 'Multi-state license uploaded without an associated single-state license of the same license type ' + 'in the same jurisdiction. Both the single-state and the multi-state license must be uploaded for ' + 'this practitioner.' +) + @sqs_handler def preprocess_license_ingest(message: dict): @@ -164,8 +170,19 @@ def ingest_license_message(message: dict): existing_license_records = [] current_provider_record = None + # Both validation checks below look up the posted license's paired single-state license via + # ProviderUserRecords, which works in terms of LicenseData, so build that view once and share it. + posted_license_data = LicenseData.create_new(deepcopy(posted_license_record)) + + # These two checks are mutually exclusive: the first only fires when the associated single-state + # license exists, the second only when it does not. _check_for_multi_state_single_state_eligibility_validation_error( - posted_license_record=posted_license_record, + posted_license_data=posted_license_data, + provider_user_records=provider_user_records, + data_events=data_events, + ) + _check_for_missing_single_state_license_validation_error( + posted_license_data=posted_license_data, provider_user_records=provider_user_records, data_events=data_events, ) @@ -341,7 +358,7 @@ def _matches_posted_license(license_record: LicenseData) -> bool: def _check_for_multi_state_single_state_eligibility_validation_error( *, - posted_license_record: dict, + posted_license_data: LicenseData, provider_user_records: ProviderUserRecords | None, data_events: list, ): @@ -349,20 +366,15 @@ def _check_for_multi_state_single_state_eligibility_validation_error( Notify the uploading jurisdiction when a multi-state license is uploaded as compact-eligible but the paired single-state license in the same jurisdiction is ineligible. The license is still persisted. """ - if posted_license_record['licenseScope'] != LicenseScopeEnum.MULTI_STATE.value: + if posted_license_data.licenseScope != LicenseScopeEnum.MULTI_STATE.value: return - if posted_license_record['jurisdictionUploadedCompactEligibility'] != CompactEligibilityStatus.ELIGIBLE: + if posted_license_data.jurisdictionUploadedCompactEligibility != CompactEligibilityStatus.ELIGIBLE: return if provider_user_records is None: return - license_type_abbr = config.license_type_abbreviations[posted_license_record['compact']][ - posted_license_record['licenseType'] - ] - associated_single_state_license = provider_user_records.get_specific_license_record( - posted_license_record['jurisdiction'], - license_type_abbr, - LicenseScopeEnum.SINGLE_STATE.value, + associated_single_state_license = provider_user_records.find_matching_single_state_license_for_multi_state_license( + posted_license_data ) if associated_single_state_license is None: return @@ -372,21 +384,70 @@ def _check_for_multi_state_single_state_eligibility_validation_error( logger.info( 'Multi-state license uploaded as eligible but associated single-state license is ineligible. ' 'Publishing license validation error event.', - provider_id=posted_license_record['providerId'], - jurisdiction=posted_license_record['jurisdiction'], - license_type=posted_license_record['licenseType'], + provider_id=posted_license_data.providerId, + jurisdiction=posted_license_data.jurisdiction, + license_type=posted_license_data.licenseType, ) data_events.append( config.event_bus_client.generate_license_validation_error_event( 'org.compactconnect.provider-data', - compact=posted_license_record['compact'], - jurisdiction=posted_license_record['jurisdiction'], - license_record=posted_license_record, + compact=posted_license_data.compact, + jurisdiction=posted_license_data.jurisdiction, + license_record=posted_license_data.to_dict(), errors={SCHEMA: [MULTI_STATE_SINGLE_STATE_ELIGIBILITY_MISMATCH_MESSAGE]}, ) ) +def _check_for_missing_single_state_license_validation_error( + *, + posted_license_data: LicenseData, + provider_user_records: ProviderUserRecords | None, + data_events: list, +): + """ + Notify the uploading jurisdiction when a multi-state license is uploaded before its associated + single-state license. The license is still persisted. + + A jurisdiction is expected to upload both the single-state and the multi-state license for a + practitioner; several downstream behaviors (CUID assignment, home jurisdiction changes) only take effect + once both are present. Uploading the multi-state license alone leaves the practitioner in that + incomplete state indefinitely, with nothing to prompt the jurisdiction to finish, so we notify them. + + The check is deliberately independent of compact eligibility and active status, matching the pairing + semantics of ``ProviderRecordUtility.has_paired_single_and_multi_state_license``. It re-fires on every + re-upload while the pairing is still missing, since the notification is the only prompt the jurisdiction + gets and the condition is still true. + """ + if posted_license_data.licenseScope != LicenseScopeEnum.MULTI_STATE.value: + return + + # No provider records at all means this upload is the provider's first license, so there is no + # single-state license for it to pair with. + if provider_user_records is not None: + associated_single_state_license = ( + provider_user_records.find_matching_single_state_license_for_multi_state_license(posted_license_data) + ) + if associated_single_state_license is not None: + return + + logger.info( + 'Multi-state license uploaded without an associated single-state license. ' + 'Publishing license validation error event.', + provider_id=posted_license_data.providerId, + jurisdiction=posted_license_data.jurisdiction, + license_type=posted_license_data.licenseType, + ) + data_events.append( + config.event_bus_client.generate_license_validation_error_event( + 'org.compactconnect.provider-data', + compact=posted_license_data.compact, + jurisdiction=posted_license_data.jurisdiction, + license_record=posted_license_data.to_dict(), + errors={SCHEMA: [MULTI_STATE_MISSING_SINGLE_STATE_MESSAGE]}, + ) + ) + def _generate_cuid(compact: str) -> str: """ Generate a new Compact Unique Identifier (CUID) for a provider. diff --git a/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py b/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py index 523a100c4..22c5156d9 100644 --- a/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py +++ b/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py @@ -1686,7 +1686,10 @@ def _setup_provider_ssn(self) -> str: @patch('handlers.ingest.EventBatchWriter', autospec=True) def test_eligible_multi_state_with_ineligible_single_state_emits_validation_error(self, mock_event_writer): - from handlers.ingest import ingest_license_message + from handlers.ingest import ( + MULTI_STATE_SINGLE_STATE_ELIGIBILITY_MISMATCH_MESSAGE, + ingest_license_message, + ) self._setup_provider_ssn() @@ -1725,7 +1728,7 @@ def test_eligible_multi_state_with_ineligible_single_state_emits_validation_erro self.assertEqual('2024-11-08T23:59:59+00:00', detail['eventTime']) self.assertNotIn('recordNumber', detail) self.assertIn('validData', detail) - self.assertIn('errors', detail) + self.assertEqual({'_schema': [MULTI_STATE_SINGLE_STATE_ELIGIBILITY_MISMATCH_MESSAGE]}, detail['errors']) self.assertEqual('multi-state', detail['validData']['licenseScope']) self.assertEqual('eligible', detail['validData']['compactEligibility']) @@ -1765,28 +1768,77 @@ def test_eligible_multi_state_with_eligible_single_state_does_not_emit_validatio mock_event_writer.return_value.__enter__.return_value.put_event.assert_not_called() - @patch('handlers.ingest.EventBatchWriter', autospec=True) - def test_eligible_multi_state_without_single_state_does_not_emit_validation_error(self, mock_event_writer): + +@mock_aws +@patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2024-11-08T23:59:59+00:00')) +class TestMultiStateMissingSingleStateValidationError(TstFunction): + """ + license.validation-error when a multi-state license is uploaded before its associated single-state license. + + A jurisdiction is expected to upload both the single-state and the multi-state license for a practitioner. + When the multi-state license arrives and no single-state license of the same type exists for that + jurisdiction, the jurisdiction is notified so it can upload the missing single-state license. The + multi-state license is still persisted. + """ + + def _ingest_license(self, detail_overrides: dict | None = None, *, message_id: str = '123') -> dict: from handlers.ingest import ingest_license_message with open('../common/tests/resources/ingest/event-bridge-message.json') as f: message = json.load(f) + if detail_overrides: + message['detail'].update(detail_overrides) + event = {'Records': [{'messageId': message_id, 'body': json.dumps(message)}]} + resp = ingest_license_message(event, self.mock_context) + self.assertEqual({'batchItemFailures': []}, resp) + return message - message['detail'].update( + def _setup_provider_ssn(self) -> str: + with open('../common/tests/resources/dynamo/provider-ssn.json') as f: + ssn_record = json.load(f) + self._ssn_table.put_item(Item=ssn_record) + return ssn_record['providerId'] + + def _get_put_entries(self, mock_event_writer) -> list[dict]: + put_event = mock_event_writer.return_value.__enter__.return_value.put_event + return [call.kwargs['Entry'] for call in put_event.call_args_list] + + def _assert_missing_single_state_error(self, entry: dict, *, jurisdiction: str, license_type: str): + from handlers.ingest import MULTI_STATE_MISSING_SINGLE_STATE_MESSAGE + + self.assertEqual('license.validation-error', entry['DetailType']) + self.assertEqual('org.compactconnect.provider-data', entry['Source']) + self.assertEqual('license-data-events', entry['EventBusName']) + + detail = json.loads(entry['Detail']) + self.assertEqual('socw', detail['compact']) + self.assertEqual(jurisdiction, detail['jurisdiction']) + self.assertEqual('2024-11-08T23:59:59+00:00', detail['eventTime']) + self.assertNotIn('recordNumber', detail) + self.assertEqual({'_schema': [MULTI_STATE_MISSING_SINGLE_STATE_MESSAGE]}, detail['errors']) + self.assertEqual('multi-state', detail['validData']['licenseScope']) + self.assertEqual(license_type, detail['validData']['licenseType']) + + @patch('handlers.ingest.EventBatchWriter', autospec=True) + def test_first_upload_of_multi_state_license_emits_validation_error(self, mock_event_writer): + """A multi-state license is the provider's very first upload, so no single-state license can exist.""" + self._setup_provider_ssn() + + message = self._ingest_license( { 'licenseScope': 'multi-state', 'licenseNumber': 'B0608337260', - 'licenseStatus': 'active', - 'compactEligibility': 'eligible', - } + }, + message_id='100', ) - event = {'Records': [{'messageId': '300', 'body': json.dumps(message)}]} - resp = ingest_license_message(event, self.mock_context) - self.assertEqual({'batchItemFailures': []}, resp) - - mock_event_writer.return_value.__enter__.return_value.put_event.assert_not_called() + entries = self._get_put_entries(mock_event_writer) + self.assertEqual(1, len(entries)) + self._assert_missing_single_state_error( + entries[0], jurisdiction='oh', license_type='licensed clinical social worker' + ) + # The license is still persisted despite the validation error provider_records = self._provider_table.query( Select='ALL_ATTRIBUTES', KeyConditionExpression=Key('pk').eq(f'socw#PROVIDER#{message["detail"]["providerId"]}'), @@ -1794,6 +1846,119 @@ def test_eligible_multi_state_without_single_state_does_not_emit_validation_erro license_records = [record for record in provider_records if record['type'] == 'license'] self.assertEqual(1, len(license_records)) self.assertEqual('multi-state', license_records[0]['licenseScope']) + self.assertEqual('B0608337260', license_records[0]['licenseNumber']) + + @patch('handlers.ingest.EventBatchWriter', autospec=True) + def test_multi_state_upload_after_associated_single_state_does_not_emit_validation_error(self, mock_event_writer): + """The expected upload order: single-state first, then the multi-state license of the same type.""" + self._setup_provider_ssn() + + self._ingest_license({'licenseScope': 'single-state'}, message_id='200') + self._ingest_license( + { + 'licenseScope': 'multi-state', + 'licenseNumber': 'B0608337260', + }, + message_id='201', + ) + + self.assertEqual([], self._get_put_entries(mock_event_writer)) + + @patch('handlers.ingest.EventBatchWriter', autospec=True) + def test_single_state_upload_without_multi_state_does_not_emit_validation_error(self, mock_event_writer): + """A jurisdiction is free to upload only a single-state license; the check is multi-state driven.""" + self._setup_provider_ssn() + + self._ingest_license({'licenseScope': 'single-state'}, message_id='300') + + self.assertEqual([], self._get_put_entries(mock_event_writer)) + + @patch('handlers.ingest.EventBatchWriter', autospec=True) + def test_single_state_license_in_other_jurisdiction_does_not_satisfy_pairing(self, mock_event_writer): + """The associated single-state license must be in the same jurisdiction as the multi-state license.""" + self._setup_provider_ssn() + + self._ingest_license({'jurisdiction': 'oh', 'licenseScope': 'single-state'}, message_id='400') + self._ingest_license( + { + 'jurisdiction': 'ky', + 'licenseScope': 'multi-state', + 'licenseNumber': 'B0608337260', + }, + message_id='401', + ) + + entries = self._get_put_entries(mock_event_writer) + self.assertEqual(1, len(entries)) + self._assert_missing_single_state_error( + entries[0], jurisdiction='ky', license_type='licensed clinical social worker' + ) + + @patch('handlers.ingest.EventBatchWriter', autospec=True) + def test_single_state_license_of_other_license_type_does_not_satisfy_pairing(self, mock_event_writer): + """The associated single-state license must be of the same license type as the multi-state license.""" + self._setup_provider_ssn() + + self._ingest_license( + {'licenseType': 'licensed clinical social worker', 'licenseScope': 'single-state'}, + message_id='500', + ) + self._ingest_license( + { + 'licenseType': 'licensed master social worker', + 'licenseScope': 'multi-state', + 'licenseNumber': 'B0608337260', + }, + message_id='501', + ) + + entries = self._get_put_entries(mock_event_writer) + self.assertEqual(1, len(entries)) + self._assert_missing_single_state_error( + entries[0], jurisdiction='oh', license_type='licensed master social worker' + ) + + @patch('handlers.ingest.EventBatchWriter', autospec=True) + def test_re_upload_of_still_unpaired_multi_state_license_emits_validation_error_again(self, mock_event_writer): + """The jurisdiction keeps being notified until it uploads the missing single-state license.""" + self._setup_provider_ssn() + + self._ingest_license( + {'licenseScope': 'multi-state', 'licenseNumber': 'B0608337260'}, + message_id='600', + ) + self._ingest_license( + {'licenseScope': 'multi-state', 'licenseNumber': 'B0608337260', 'familyName': 'VonSmitherton'}, + message_id='601', + ) + + entries = self._get_put_entries(mock_event_writer) + self.assertEqual(2, len(entries)) + for entry in entries: + self._assert_missing_single_state_error( + entry, jurisdiction='oh', license_type='licensed clinical social worker' + ) + + @patch('handlers.ingest.EventBatchWriter', autospec=True) + def test_ineligible_multi_state_license_without_single_state_emits_validation_error(self, mock_event_writer): + """The missing-pair check does not depend on the compact eligibility of the multi-state license.""" + self._setup_provider_ssn() + + self._ingest_license( + { + 'licenseScope': 'multi-state', + 'licenseNumber': 'B0608337260', + 'licenseStatus': 'inactive', + 'compactEligibility': 'ineligible', + }, + message_id='700', + ) + + entries = self._get_put_entries(mock_event_writer) + self.assertEqual(1, len(entries)) + self._assert_missing_single_state_error( + entries[0], jurisdiction='oh', license_type='licensed clinical social worker' + ) @mock_aws From 4905026bae55a2ae0e5ab03dbb583a321c2a2668 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 6 Aug 2026 15:48:20 -0500 Subject: [PATCH 2/4] Update staff user docs to reflect need to upload single state licenses first --- backend/cosmetology-app/docs/README.md | 8 ++++- backend/social-work-app/docs/README.md | 45 ++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/backend/cosmetology-app/docs/README.md b/backend/cosmetology-app/docs/README.md index 6f0a9fbca..6390bc529 100644 --- a/backend/cosmetology-app/docs/README.md +++ b/backend/cosmetology-app/docs/README.md @@ -103,7 +103,13 @@ If data is not available for a required field, that particular license record ca ### Can we upload the same licenses multiple times? What if their information changes? -Yes. CompactConnect is designed to automatically detect and track changes to license records over time. When you upload a license record, CompactConnect will determine if the record currently exists in the CompactConnect database using the provided SSN to match with any existing licensee in the system, and create the record if not found. If the license record already exists, CompactConnect will check the differences between the existing record in the system and changes uploaded by the state, and apply the changes accordingly. +Yes. CompactConnect is designed to automatically detect and track changes to license records over time. The Social Security Number (SSN) is the unique identifier CompactConnect uses to create and match individual practitioner accounts. When you upload a license record, CompactConnect uses the provided SSN to determine whether that practitioner already has an account in the system, creating one if not found. If the practitioner's account already exists, CompactConnect will check the differences between the existing license record and the changes uploaded by the state, and apply the changes accordingly. + +Because accounts are matched on SSN, simply changing the SSN in your state's system and then uploading the corrected license will **not** update the practitioner's existing CompactConnect account. It will create a brand new, separate account under the new SSN and leave the original account (and any privileges tied to it) unchanged. + +> **⚠️ Verify SSNs before you upload.** The SSN is the sole identifier CompactConnect uses to match a license to a practitioner's account, and every downstream consequence of an upload (account creation, privilege eligibility, public lookup, etc.) follows from it. Uploading an incorrect SSN is not a low-risk mistake to leave unaddressed, as it silently creates or attaches records to the wrong account, fragmenting the practitioner's licensure history and leaving privileges tied to whichever account was in place at the time they were purchased. + +**If your state has uploaded a license with an incorrect SSN, contact CSG support. ### Which of these license values will be publicly visible? diff --git a/backend/social-work-app/docs/README.md b/backend/social-work-app/docs/README.md index b93d6b022..8d5118d4a 100644 --- a/backend/social-work-app/docs/README.md +++ b/backend/social-work-app/docs/README.md @@ -30,6 +30,8 @@ Export your license data to a CSV file, formatted as follows: - Some fields have a set list of allowed values. For those fields, make sure to enter the value exactly, including spacing and capitalization - SSNs must be unique within a single CSV upload file. Do not include multiple rows with the same `ssn` in one file. If duplicate SSNs are sent within the same file, the first row will be processed, but all other duplicate rows will be rejected. + - If a practitioner holds a multi-state license, you must upload their single-state license **first**. See + [Upload order: single-state licenses before multi-state licenses](#upload-order-single-state-licenses-before-multi-state-licenses). #### Field Descriptions @@ -51,7 +53,7 @@ leave the field entirely empty. If some of your licenses are missing a required | homeAddressStreet1* | First line of provider's street address | String (max 100 chars) | 123 Main St | | licenseNumber* | License number | String (max 100 chars) | OT12345 | | licenseType* | Type of professional license. Types you provide must be associated with the compact you are uploading for. | One of: `licensed clinical social worker`, `licensed master social worker`, `licensed bachelors social worker` | licensed clinical social worker | -| licenseScope* | Whether the license is a single-state or multi-state license under the compact. Multi-state licenses in a provider's home jurisdiction can generate privileges to practice in other compact member states. | One of: `single-state`, `multi-state` | single-state | +| licenseScope* | Whether the license is a single-state or multi-state license under the compact. Multi-state licenses in a provider's home jurisdiction can generate privileges to practice in other compact member states. *Note: a practitioner's single-state license must be uploaded before their multi-state license of the same license type. See [Upload order](#upload-order-single-state-licenses-before-multi-state-licenses).* | One of: `single-state`, `multi-state` | single-state | | ssn* | Social Security Number | Format: XXX-XX-XXXX | 123-45-6789 | | licenseStatus* | Current status of the license. "active" means they are allowed to practice their profession. *Note: licenses will automatically be displayed as `inactive` after their date of expiration, even if the last upload still showed them as `active`.* | One of: `active`, `inactive` | active | | licenseStatusName | An optional more descriptive name of the license status. | String (max 100 chars) | SUSPENDED | @@ -65,12 +67,36 @@ leave the field entirely empty. If some of your licenses are missing a required ```csv dateOfIssuance,licenseNumber,dateOfBirth,licenseType,licenseScope,familyName,homeAddressCity,middleName,licenseStatus,licenseStatusName,compactEligibility,ssn,homeAddressStreet1,homeAddressStreet2,dateOfExpiration,homeAddressState,homeAddressPostalCode,givenName,dateOfRenewal 2024-06-30,A0608337260,2024-06-30,licensed clinical social worker,single-state,Guðmundsdóttir,Birmingham,Gunnar,active,ACTIVE,eligible,529-31-5408,123 A St.,Apt 321,2024-06-30,oh,35004,Björk,2024-06-30 -2024-06-30,B0608337260,2024-06-30,licensed master social worker,multi-state,Scott,Huntsville,Patricia,active,ACTIVE,eligible,529-31-5409,321 B St.,,2024-06-30,oh,35005,Elizabeth,2024-06-30 +2024-06-30,B0608337260,2024-06-30,licensed master social worker,single-state,Scott,Huntsville,Patricia,active,ACTIVE,eligible,529-31-5409,321 B St.,,2024-06-30,oh,35005,Elizabeth,2024-06-30 2024-06-30,C0608337260,2024-06-30,licensed clinical social worker,single-state,毛,Hoover,泽,active,ACTIVE,eligible,529-31-5410,10101 Binary Ave.,,2024-06-30,oh,35006,覃,2024-06-30 2024-06-30,D0608337260,2024-06-30,licensed clinical social worker,single-state,Adams,Tuscaloosa,Michael,inactive,EXPIRED,ineligible,529-31-5411,1AB3 Hex Blvd.,,2024-06-30,oh,35007,John,2024-06-30 2024-06-30,E0608337260,2024-06-30,licensed clinical social worker,single-state,Carreño Quiñones,Montgomery,José,active,ACTIVE_IN_RENEWAL,eligible,529-31-5412,10 Main St.,,2024-06-30,oh,35008,María,2024-06-30 ``` +### Upload order: single-state licenses before multi-state licenses + +**A practitioner's single-state license must be fully ingested before you upload their multi-state license.** + +In CompactConnect, a single-state license and a multi-state license are two separate records, even for the same +practitioner. A multi-state license is *associated* with a single-state license when all three of the following match: + +- the same practitioner (matched on `ssn`), +- the same jurisdiction (your state), and +- the same `licenseType` (for example, both are `licensed clinical social worker`). + +A multi-state license is only meaningful to the compact alongside the single-state license it is built on. Until both +records are present, CompactConnect cannot treat the practitioner as compact-eligible through that multi-state license: +the practitioner will not be issued a compact identifier, and the multi-state license will not be used to determine +their home jurisdiction or to generate privileges in other member states. + +#### What happens if you upload the multi-state license first + +The multi-state license is **not rejected**. However, the upload is flagged as a validation +error, and that error is included in a data ingest error report emailed to your state's operations contact. + +This notification is sent again on every subsequent upload of that multi-state license until the associated +single-state license is uploaded. Uploading the missing single-state license resolves it. + ### Manual Uploads 1) Request a staff user with permissions you need. @@ -104,7 +130,20 @@ If data is not available for a required field, that particular license record ca ### Can we upload the same licenses multiple times? What if their information changes? -Yes. CompactConnect is designed to automatically detect and track changes to license records over time. When you upload a license record, CompactConnect will determine if the record currently exists in the CompactConnect database using the provided SSN to match with any existing licensee in the system, and create the record if not found. If the license record already exists, CompactConnect will check the differences between the existing record in the system and changes uploaded by the state, and apply the changes accordingly. +Yes. CompactConnect is designed to automatically detect and track changes to license records over time. The Social Security Number (SSN) is the unique identifier CompactConnect uses to create and match individual practitioner accounts. When you upload a license record, CompactConnect uses the provided SSN to determine whether that practitioner already has an account in the system, creating one if not found. If the practitioner's account already exists, CompactConnect will check the differences between the existing license record and the changes uploaded by the state, and apply the changes accordingly. + +Because accounts are matched on SSN, simply changing the SSN in your state's system and then uploading the corrected license will **not** update the practitioner's existing CompactConnect account. It will create a brand new, separate account under the new SSN and leave the original account (and any privileges tied to it) unchanged. + +> **⚠️ Verify SSNs before you upload.** The SSN is the sole identifier CompactConnect uses to match a license to a practitioner's account, and every downstream consequence of an upload (account creation, privilege eligibility, public lookup, etc.) follows from it. Uploading an incorrect SSN is not a low-risk mistake to leave unaddressed, as it silently creates or attaches records to the wrong account, fragmenting the practitioner's licensure history and leaving privileges tied to whichever account was in place at the time they were purchased. + +**If your state has uploaded a license with an incorrect SSN, contact CSG support. + + + +### Do we need to upload a single-state license for every practitioner who has a multi-state license? + +Yes. A multi-state license does not replace the single-state license it is built on, and CompactConnect stores them as +two separate records. A practitioner with a multi-state license should always have both records in the system. ### Which of these license values will be publicly visible? From 802396106cb27b5fa5bf2cf60eeb4098d8506ea8 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 6 Aug 2026 15:56:36 -0500 Subject: [PATCH 3/4] Close Markdown bold span --- backend/cosmetology-app/docs/README.md | 2 +- backend/social-work-app/docs/README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/cosmetology-app/docs/README.md b/backend/cosmetology-app/docs/README.md index 6390bc529..9bb27d98d 100644 --- a/backend/cosmetology-app/docs/README.md +++ b/backend/cosmetology-app/docs/README.md @@ -109,7 +109,7 @@ Because accounts are matched on SSN, simply changing the SSN in your state's sys > **⚠️ Verify SSNs before you upload.** The SSN is the sole identifier CompactConnect uses to match a license to a practitioner's account, and every downstream consequence of an upload (account creation, privilege eligibility, public lookup, etc.) follows from it. Uploading an incorrect SSN is not a low-risk mistake to leave unaddressed, as it silently creates or attaches records to the wrong account, fragmenting the practitioner's licensure history and leaving privileges tied to whichever account was in place at the time they were purchased. -**If your state has uploaded a license with an incorrect SSN, contact CSG support. +**If your state has uploaded a license with an incorrect SSN, contact CSG support.** ### Which of these license values will be publicly visible? diff --git a/backend/social-work-app/docs/README.md b/backend/social-work-app/docs/README.md index 8d5118d4a..0ca77f7fb 100644 --- a/backend/social-work-app/docs/README.md +++ b/backend/social-work-app/docs/README.md @@ -86,8 +86,8 @@ practitioner. A multi-state license is *associated* with a single-state license A multi-state license is only meaningful to the compact alongside the single-state license it is built on. Until both records are present, CompactConnect cannot treat the practitioner as compact-eligible through that multi-state license: -the practitioner will not be issued a compact identifier, and the multi-state license will not be used to determine -their home jurisdiction or to generate privileges in other member states. +the practitioner will not be issued a public compact identifier, and the multi-state license will not be used to determine +their home jurisdiction or to generate privileges to practice in other member states. #### What happens if you upload the multi-state license first @@ -136,7 +136,7 @@ Because accounts are matched on SSN, simply changing the SSN in your state's sys > **⚠️ Verify SSNs before you upload.** The SSN is the sole identifier CompactConnect uses to match a license to a practitioner's account, and every downstream consequence of an upload (account creation, privilege eligibility, public lookup, etc.) follows from it. Uploading an incorrect SSN is not a low-risk mistake to leave unaddressed, as it silently creates or attaches records to the wrong account, fragmenting the practitioner's licensure history and leaving privileges tied to whichever account was in place at the time they were purchased. -**If your state has uploaded a license with an incorrect SSN, contact CSG support. +**If your state has uploaded a license with an incorrect SSN, contact CSG support.** From 02098384dfd6b2dab0a785ca315e69d291077a55 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 6 Aug 2026 15:59:20 -0500 Subject: [PATCH 4/4] revise wording for error reporting --- backend/social-work-app/docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/social-work-app/docs/README.md b/backend/social-work-app/docs/README.md index 0ca77f7fb..f3dade2a9 100644 --- a/backend/social-work-app/docs/README.md +++ b/backend/social-work-app/docs/README.md @@ -91,7 +91,7 @@ their home jurisdiction or to generate privileges to practice in other member st #### What happens if you upload the multi-state license first -The multi-state license is **not rejected**. However, the upload is flagged as a validation +The multi-state license is **not rejected**. However, the upload is flagged as an error, and that error is included in a data ingest error report emailed to your state's operations contact. This notification is sent again on every subsequent upload of that multi-state license until the associated