diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index 71a0d22ca..07ef82f65 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -44,9 +44,7 @@ jobs: env: S3_ENV: ${{ github.event_name == 'workflow_dispatch' && (inputs.environment || 'internal-qa') }} - steps: - - name: Connect to AWS uses: aws-actions/configure-aws-credentials@v4 with: @@ -95,6 +93,8 @@ jobs: TPP_client_Secret: ${{ secrets.TPP_client_Secret }} SONAR_client_Id: ${{ secrets.SONAR_client_Id }} SONAR_client_Secret: ${{ secrets.SONAR_client_Secret }} + MEDICUS_client_Id: ${{ secrets.MEDICUS_client_Id }} + MEDICUS_client_Secret: ${{ secrets.MEDICUS_client_Secret }} aws_token_refresh: 'False' PYTHONPATH: ${{ github.workspace }} run: | @@ -120,8 +120,6 @@ jobs: echo "Pytest-BDD tests passed" fi - - - name: Publish test results to GitHub UI if: always() uses: dorny/test-reporter@v1 diff --git a/features/APITests/create.feature b/features/APITests/create.feature index 7b2dd7318..4c96444f0 100644 --- a/features/APITests/create.feature +++ b/features/APITests/create.feature @@ -19,13 +19,20 @@ Scenario Outline: Verify that the POST Create API for different vaccine types |Random | FLU | MAVIS | |Random | MMR | Postman_Auth | |Random | MENACWY | TPP | - |Random | 3in1 | TPP | + |Random | 3IN1 | TPP | |Random | MMRV | EMIS | |Random | PERTUSSIS | EMIS | |Random | SHINGLES | EMIS | |Random | PNEUMOCOCCAL| EMIS | - -@Delete_cleanUp @vaccine_type_RSV @patient_id_Random @supplier_name_RAVS + |Random | 4IN1 | TPP | + |Random | 6IN1 | EMIS | + |Random | HIB | TPP | + |Random | MENB | TPP | + |Random | ROTAVIRUS | MEDICUS | + |Random | HEPB | EMIS | + |Random | BCG | MEDICUS | + +@Delete_cleanUp @vaccine_type_6IN1 @patient_id_Random @supplier_name_EMIS Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM, SITE_OF_VACCINATION_TERM, ROUTE_OF_VACCINATION_TERM fields are mapped to respective text fields in imms delta table Given Valid json payload is created where vaccination terms has text field populated When Trigger the post create request @@ -33,7 +40,7 @@ Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM, SITE_OF_ And The location key and Etag in header will contain the Immunization Id and version And The terms are mapped to the respective text fields in imms delta table -@Delete_cleanUp @vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +@Delete_cleanUp @vaccine_type_BCG @patient_id_Random @supplier_name_EMIS Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM fields are mapped to first instance of coding.display fields in imms delta table Given Valid json payload is created where vaccination terms has multiple instances of coding When Trigger the post create request @@ -41,7 +48,7 @@ Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM fields ar And The location key and Etag in header will contain the Immunization Id and version And The terms are mapped to first instance of coding.display fields in imms delta table -@Delete_cleanUp @vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +@Delete_cleanUp @vaccine_type_HEPB @patient_id_Random @supplier_name_MEDICUS Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM, SITE_OF_VACCINATION_TERM, ROUTE_OF_VACCINATION_TERM fields are mapped to correct instance of coding.display fields in imms delta table Given Valid json payload is created where vaccination terms has multiple instance of coding with different coding system When Trigger the post create request @@ -49,7 +56,7 @@ Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM, SITE_OF_ And The location key and Etag in header will contain the Immunization Id and version And The terms are mapped to correct instance of coding.display fields in imms delta table -@Delete_cleanUp @vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +@Delete_cleanUp @vaccine_type_PERTUSSIS @patient_id_Random @supplier_name_EMIS Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM, SITE_OF_VACCINATION_TERM, ROUTE_OF_VACCINATION_TERM fields are mapped to coding.display in imms delta table in case of only one instance of coding Given Valid json payload is created where vaccination terms has one instance of coding with no text or value string field When Trigger the post create request @@ -57,7 +64,7 @@ Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM, SITE_OF_ And The location key and Etag in header will contain the Immunization Id and version And The terms are mapped to correct coding.display fields in imms delta table -@Delete_cleanUp @vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +@Delete_cleanUp @vaccine_type_HIB @patient_id_Random @supplier_name_TPP Scenario: Verify that VACCINATION_PROCEDURE_TERM, VACCINE_PRODUCT_TERM, SITE_OF_VACCINATION_TERM, ROUTE_OF_VACCINATION_TERM fields are blank in imms delta table if no text or value string or display field is present Given Valid json payload is created where vaccination terms has no text or value string or display field When Trigger the post create request @@ -73,7 +80,7 @@ Scenario Outline: Verify that the POST Create API for different supplier fails And The Response JSONs should contain correct error message for 'unauthorized_access' access Examples: | Patient | vaccine_type| Supplier | - |Random | COVID | MAVIS | + |Random | COVID | MAVIS | |Random | RSV | MAVIS | |Random | RSV | SONAR | diff --git a/features/APITests/read.feature b/features/APITests/read.feature index 678a04a3c..08766d0bc 100644 --- a/features/APITests/read.feature +++ b/features/APITests/read.feature @@ -1,7 +1,7 @@ @Read_Feature @functional Feature: Read the immunization of a patient -@Delete_cleanUp @supplier_name_Postman_Auth +@Delete_cleanUp @supplier_name_MEDICUS Scenario Outline: Verify that the Read method of API will be successful and fetch valid imms event detail Given Valid vaccination record is created with Patient '' and vaccine_type '' When Send a read request for Immunization event created @@ -12,9 +12,9 @@ Scenario Outline: Verify that the Read method of API will be successful and fetc Examples: |Patient | Vaccine_type| - |Random | RSV | + |Random | 4IN1 | |Random | FLU | - |Random | COVID | + |Random | COVID | @Delete_cleanUp @vaccine_type_FLU @patient_id_Random @supplier_name_Postman_Auth diff --git a/features/APITests/search.feature b/features/APITests/search.feature index 1c45746e3..1a3270951 100644 --- a/features/APITests/search.feature +++ b/features/APITests/search.feature @@ -2,7 +2,7 @@ Feature: Search the immunization of a patient @smoke -@Delete_cleanUp @supplier_name_Postman_Auth +@Delete_cleanUp @supplier_name_TPP Scenario Outline: Verify that the GET method of Search API will be successful with all the valid parameters Given Valid vaccination record is created with Patient '' and vaccine_type '' When Send a search request with GET method for Immunization event created @@ -26,7 +26,7 @@ Scenario Outline: Verify that the GET method of Search API will be successful wi |Random | PNEUMOCOCCAL | @smoke -@Delete_cleanUp @supplier_name_Postman_Auth +@Delete_cleanUp @supplier_name_EMIS Scenario Outline: Verify that the POST method of Search API will be successful with all the valid parameters Given Valid vaccination record is created with Patient '' and vaccine_type '' When Send a search request with POST method for Immunization event created @@ -37,14 +37,14 @@ Scenario Outline: Verify that the POST method of Search API will be successful w Examples: |Patient | Vaccine_type| |Random | RSV | - |SFlag | RSV | - |SupersedeNhsNo| RSV | + |SFlag | SHINGLES | + |SupersedeNhsNo| PERTUSSIS | |Random | FLU | - |SFlag | FLU | - |SupersedeNhsNo| FLU | + |SFlag | 3IN1 | + |SupersedeNhsNo| 4IN1 | |Random | COVID | - |SFlag | COVID | - |SupersedeNhsNo| COVID | + |SFlag | BCG | + |SupersedeNhsNo| HEPB | @supplier_name_Postman_Auth Scenario Outline: Verify that the immunisation events retrieved in the response of Search API should be within Date From and Date To range diff --git a/features/APITests/steps/test_delete_steps.py b/features/APITests/steps/test_delete_steps.py index 78983baa6..fb79999da 100644 --- a/features/APITests/steps/test_delete_steps.py +++ b/features/APITests/steps/test_delete_steps.py @@ -28,7 +28,7 @@ def send_delete_for_immunization_event_by_supplier(context, Supplier): send_delete_for_immunization_event_created(context) @then('The delta table will be populated with the correct data for deleted event') -def validate_imms_delta_table_by_ImmsID(context): +def validate_imms_delta_table_by_deleted_ImmsID(context): create_obj = context.create_object items = fetch_immunization_int_delta_detail_by_immsID(context.aws_profile_name, context.ImmsID, context.S3_env, 2) assert items, f"Items not found in response for ImmsID: {context.ImmsID}" diff --git a/features/APITests/update.feature b/features/APITests/update.feature index f058e92c5..c73bd4095 100644 --- a/features/APITests/update.feature +++ b/features/APITests/update.feature @@ -117,13 +117,13 @@ Scenario Outline: Verify that the Update API will be fails if patient's date of | nonexistent | invalid_DateOfBirth | | empty | invalid_DateOfBirth | -@vaccine_type_3in1 @patient_id_Random @supplier_name_Postman_Auth +@vaccine_type_3IN1 @patient_id_Random @supplier_name_Postman_Auth Scenario: Verify that the update request will fail for invalid immunization id When Send an update request for invalid immunization id Then The request will be unsuccessful with the status code '404' And The Response JSONs should contain correct error message for 'not_found' -@vaccine_type_3in1 @patient_id_Random @supplier_name_Postman_Auth +@vaccine_type_3IN1 @patient_id_Random @supplier_name_Postman_Auth Scenario Outline: Verify that the update request will fail for invalid Etag value When Send an update request for invalid Etag Then The request will be unsuccessful with the status code '400' diff --git a/features/batchTests/Steps/batch_common_steps.py b/features/batchTests/Steps/batch_common_steps.py index 5aed7cb3a..402415a67 100644 --- a/features/batchTests/Steps/batch_common_steps.py +++ b/features/batchTests/Steps/batch_common_steps.py @@ -39,7 +39,6 @@ def wrapper(*args, **kwargs): file_name = os.getenv("LOCAL_RUN_FILE_NAME") context.filename = file_name - context.expected_version = "1" file_path = os.path.join(context.working_directory, file_name) # Read file into vaccine_df @@ -66,15 +65,6 @@ def valid_batch_file_is_created_with_details(datatable, context): build_dataFrame_using_datatable(datatable, context) create_batch_file(context) -@given("Valid batch file is created") -def valid_batch_file_is_created(context): - context.file_extension = "csv" - context.filename = generate_file_name(context) - record = build_batch_file(context) - context.vaccine_df = pd.DataFrame([record.dict()]) - save_record_to_batch_files_directory(context) - print(f"Batch file created: {context.filename}") - @when("same batch file is uploaded again in s3 bucket") @when("batch file is uploaded in s3 bucket") @ignore_local_run_set_test_data @@ -83,12 +73,6 @@ def batch_file_upload_in_s3_bucket(context): print(f"Batch file uploaded to S3: {context.filename}") fileIsMoved = wait_for_file_to_move_archive(context) assert fileIsMoved, f"File not found in archive after timeout" - -# @when("same batch file is uploaded again in s3 bucket") -# @ignore_local_run_set_test_data -# def same_batch_file_upload_in_s3_bucket(context): -# time.sleep(30) -# batch_file_upload_in_s3_bucket(context) @then("file will be moved to destination bucket and inf ack file will be created") def file_will_be_moved_to_destination_bucket(context): @@ -105,7 +89,7 @@ def file_will_be_moved_to_destination_bucket(context): context.fileContent = wait_and_read_ack_file(context, "forwardedFile") assert context.fileContent, f"File not found in destination bucket after timeout: {context.forwarded_prefix}" -@then("bus ack will be empty as all records are processed successfully") +@then("bus ack will not have any entry of successfully processed records") def all_records_are_processed_successfully_in_the_batch_file(context): file_rows = read_and_validate_bus_ack_file_content(context) all_valid = validate_bus_ack_file_for_successful_records(context, file_rows) @@ -119,52 +103,31 @@ def validate_imms_audit_table(context): item = table_query_response[0] validate_audit_table_record(context, item, "Processed") -@then("The delta table will be populated with the correct data for all records in batch file") -def validate_imms_delta_table_for_all_records_in_batch_file(context): - df = context.vaccine_df - - # Defensive check - check.is_true("IMMS_ID" in df.columns, "Column 'IMMS_ID' not found in vaccine_df") - - # Filter rows where IMMS_ID is not null - valid_rows = df[df["IMMS_ID"].notnull()] - - check.is_true(not valid_rows.empty, "No rows with non-null IMMS_ID found in vaccine_df") - - for _, row in valid_rows.iterrows(): - imms_id = row["IMMS_ID"] - context.ImmsID= imms_id.replace("Immunization#", "") - batch_record = {k: normalize(v) for k, v in row.to_dict().items()} - - item = fetch_immunization_int_delta_detail_by_immsID( - context.aws_profile_name, - context.ImmsID, - context.S3_env - ) - - check.is_true(item, f"Item not found in response for IMMS_ID: {imms_id}") - - if item: - validate_imms_delta_record_with_batch_record( - context, - batch_record, - item, - Operation.created.value, - ActionFlag.created.value - ) +@then("The delta table will be populated with the correct data for all created records in batch file") +def validate_imms_delta_table_for_created_records_in_batch_file(context): + preload_delta_data(context) + validate_imms_delta_table_for_newly_created_records_in_batch_file(context) + +@then("The delta table will be populated with the correct data for all updated records in batch file") +def validate_imms_delta_table_for_updated_records(context): + if context.delta_cache is None: + preload_delta_data(context) + validate_imms_delta_table_for_updated_records_in_batch_file(context) + +@then("The delta table will be populated with the correct data for all deleted records in batch file") +def validate_imms_delta_table_for_deleted_records(context): + if context.delta_cache is None: + preload_delta_data(context) + validate_imms_delta_table_for_deleted_records_in_batch_file(context) @then(parsers.parse("The imms event table will be populated with the correct data for '{operation}' event for records in batch file")) def validate_imms_event_table_for_all_records_in_batch_file(context, operation: Operation): - df = context.vaccine_df - - df["UNIQUE_ID_COMBINED"] = df["UNIQUE_ID_URI"].astype(str) + "#" + df["UNIQUE_ID"].astype(str) - valid_rows = df[df["UNIQUE_ID_COMBINED"].notnull() & (df["UNIQUE_ID_COMBINED"] != "nan#nan")] - + mapping = ActionMap[operation.lower()] + df = context.vaccine_df[context.vaccine_df["ACTION_FLAG"].str.lower() == mapping.action_flag.value.lower()] + df["UNIQUE_ID_COMBINED"] = df["UNIQUE_ID_URI"].astype(str) + "#" + df["UNIQUE_ID"].astype(str) valid_rows = df[df["UNIQUE_ID_COMBINED"].notnull() & (df["UNIQUE_ID_COMBINED"] != "nan#nan")] - for idx, row in valid_rows.iterrows(): - unique_id_combined = row["UNIQUE_ID_COMBINED"] for idx, row in valid_rows.iterrows(): unique_id_combined = row["UNIQUE_ID_COMBINED"] batch_record = {k: normalize(v) for k, v in row.to_dict().items()} @@ -179,17 +142,7 @@ def validate_imms_event_table_for_all_records_in_batch_file(context, operation: df.at[idx, "IMMS_ID"]= item.get("PK") context.ImmsID= item.get("PK").replace("Immunization#", "") - - table_query_response = fetch_immunization_events_detail_by_IdentifierPK( - context.aws_profile_name, unique_id_combined, context.S3_env - ) - assert "Items" in table_query_response and table_query_response["Count"] > 0, \ - f"Item not found in response for unique_id_combined: {unique_id_combined}" - - item = table_query_response["Items"][0] - - df.at[idx, "IMMS_ID"]= item.get("PK") - context.ImmsID= item.get("PK").replace("Immunization#", "") + update_imms_id_for_all_related_rows(context.vaccine_df, unique_id_combined, context.ImmsID) resource_json_str = item.get("Resource") assert resource_json_str, "Resource field missing in item." @@ -204,7 +157,7 @@ def validate_imms_event_table_for_all_records_in_batch_file(context, operation: created_event = parse_imms_int_imms_event_response(resource) nhs_number = batch_record.get("NHS_NUMBER") or "TBC" - + fields_to_compare = [ ("Operation", Operation[operation].value, item.get("Operation")), ("SupplierSystem", context.supplier_name, item.get("SupplierSystem")), @@ -221,13 +174,19 @@ def validate_imms_event_table_for_all_records_in_batch_file(context, operation: validate_to_compare_batch_record_with_event_table_record(context, batch_record, created_event) + +@then("all records are rejected in the bus ack file and no imms id is generated") +def all_record_are_rejected_for_given_field_name(context): + file_rows = read_and_validate_bus_ack_file_content(context) + all_valid = validate_bus_ack_file_for_error(context, file_rows) + assert all_valid, "One or more records failed validation checks" def normalize(value): return "" if pd.isna(value) or value == "" else value def create_batch_file(context, file_ext: str = "csv", fileName: str = None, delimiter: str = "|"): - context.expected_version = "1" - context.FileTimestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S") + f"{int(datetime.now(timezone.utc).microsecond / 10000):02d}" + offset = datetime.now().astimezone().strftime("%z")[-2:] + context.FileTimestamp = datetime.now().astimezone().strftime("%Y%m%dT%H%M%S") + offset context.file_extension = file_ext timestamp_pattern = r'\d{8}T\d{8}' @@ -262,4 +221,107 @@ def build_dataFrame_using_datatable(datatable, context): context.vaccine_df = pd.DataFrame(records) +def update_imms_id_for_all_related_rows(df, unique_id_combined, imms_id): + mask = (df["UNIQUE_ID_URI"].astype(str) + "#" + df["UNIQUE_ID"].astype(str)) == unique_id_combined + df.loc[mask, "IMMS_ID"] = imms_id + +def preload_delta_data(context): + df = context.vaccine_df + + check.is_true("IMMS_ID" in df.columns, "Column 'IMMS_ID' not found in vaccine_df") + + valid_rows = df[df["IMMS_ID"].notnull()] + check.is_true(not valid_rows.empty, "No rows with non-null IMMS_ID found in vaccine_df") + + grouped = valid_rows.groupby("IMMS_ID") + + context.delta_cache = {} + + for imms_id, group in grouped: + clean_id = imms_id.replace("Immunization#", "") + delta_items = fetch_immunization_int_delta_detail_by_immsID( + context.aws_profile_name, + clean_id, + context.S3_env + ) + check.is_true(delta_items, f"No delta records returned for IMMS_ID: {clean_id}") + + context.delta_cache[clean_id] = { + "rows": group, + "delta_items": delta_items + } + +def validate_imms_delta_table_for_newly_created_records_in_batch_file(context): + for clean_id, data in context.delta_cache.items(): + rows = data["rows"] + delta_items = data["delta_items"] + + create_items = [i for i in delta_items if i.get("Operation") == "CREATE"] + + check.is_true( + len(create_items) == 1, + f"Expected exactly 1 CREATE record for IMMS_ID {clean_id}, found {len(create_items)}" + ) + + create_item = create_items[0] + + for _, row in rows[rows["ACTION_FLAG"] == "NEW"].iterrows(): + batch_record = {k: normalize(v) for k, v in row.to_dict().items()} + + validate_imms_delta_record_with_batch_record( + context, + batch_record, + create_item, + Operation.created.value, + ActionFlag.created.value + ) + +def validate_imms_delta_table_for_updated_records_in_batch_file(context): + for clean_id, data in context.delta_cache.items(): + rows = data["rows"] + delta_items = data["delta_items"] + + update_items = [i for i in delta_items if i.get("Operation") == "UPDATE"] + check.is_true(update_items, f"No UPDATE records for IMMS_ID {clean_id}") + updated_index = context.expected_version - 2 + for _, row in rows[rows["ACTION_FLAG"] == "UPDATE"].iterrows(): + batch_record = {k: normalize(v) for k, v in row.to_dict().items()} + item = update_items.pop(updated_index) + + validate_imms_delta_record_with_batch_record( + context, + batch_record, + item, + Operation.updated.value, + ActionFlag.updated.value + ) + +def validate_imms_delta_table_for_deleted_records_in_batch_file(context): + for clean_id, data in context.delta_cache.items(): + rows = data["rows"] + delta_items = data["delta_items"] + + delete_item = next( + (i for i in delta_items if i.get("Operation") == "DELETE"), + None + ) + + check.is_true(delete_item, f"No DELETE record for IMMS_ID {clean_id}") + + delete_rows = rows[rows["ACTION_FLAG"] == "DELETE"] + + check.is_true( + len(delete_rows) == 1, + f"Expected exactly 1 DELETE row in batch file for IMMS_ID {clean_id}, found {len(delete_rows)}" + ) + + row = delete_rows.iloc[0] + batch_record = {k: normalize(v) for k, v in row.to_dict().items()} + validate_imms_delta_record_with_batch_record( + context, + batch_record, + delete_item, + Operation.deleted.value, + ActionFlag.deleted.value + ) \ No newline at end of file diff --git a/features/batchTests/Steps/test_create_batch_steps.py b/features/batchTests/Steps/test_create_batch_steps.py index 6dacf00c3..4de4cb991 100644 --- a/features/batchTests/Steps/test_create_batch_steps.py +++ b/features/batchTests/Steps/test_create_batch_steps.py @@ -179,10 +179,4 @@ def valid_batch_file_is_created_with_missing_non_mandatory_fields(datatable, con context.vaccine_df.loc[11, "DOSE_UNIT_CODE"] = "" context.vaccine_df.loc[12, "DOSE_UNIT_TERM"] = "" context.vaccine_df.loc[13, "INDICATION_CODE"] = "" - create_batch_file(context) - -@then("all records are rejected in the bus ack file and no imms id is generated") -def all_record_are_rejected_for_given_field_name(context): - file_rows = read_and_validate_bus_ack_file_content(context) - all_valid = validate_bus_ack_file_for_error(context, file_rows) - assert all_valid, "One or more records failed validation checks" \ No newline at end of file + create_batch_file(context) \ No newline at end of file diff --git a/features/batchTests/Steps/test_delete_batch_steps.py b/features/batchTests/Steps/test_delete_batch_steps.py new file mode 100644 index 000000000..4a87b50a7 --- /dev/null +++ b/features/batchTests/Steps/test_delete_batch_steps.py @@ -0,0 +1,97 @@ +from multiprocessing import context +from src.dynamoDB.dynamo_db_helper import * +from src.objectModels.api_immunization_builder import * +from src.objectModels.batch.batch_file_builder import * +from utilities.batch_S3_buckets import * +from utilities.batch_file_helper import * +from utilities.date_helper import * +from utilities.text_helper import get_text +from utilities.vaccination_constants import * +from pytest_bdd import scenarios, given, when, then, parsers +import pytest_check as check +from .batch_common_steps import * +from features.APITests.steps.common_steps import * +from features.APITests.steps.test_create_steps import validate_imms_delta_table_by_ImmsID +from features.APITests.steps.test_delete_steps import validate_imms_delta_table_by_deleted_ImmsID + +scenarios('batchTests/delete_batch.feature') + +@given("batch file is created for below data as full dataset and each record has a valid delete record in the same file") +@ignore_if_local_run +def valid_batch_file_is_created_with_details(datatable, context): + build_dataFrame_using_datatable(datatable, context) + df_new = context.vaccine_df.copy() + df_update = df_new.copy() + df_update["ACTION_FLAG"] = "DELETE" + context.vaccine_df = pd.concat([df_new, df_update], ignore_index=True) + create_batch_file(context) + +@given("I have created a valid vaccination record through API") +def create_valid_vaccination_record_through_api(context): + validVaccinationRecordIsCreated(context) + print(f"Created Immunization record with ImmsID: {context.ImmsID}") + +@when("An delete to above vaccination record is made through batch file upload") +def upload_batch_file_to_s3_for_update(context): + record = build_batch_file(context) + context.vaccine_df = pd.DataFrame([record.dict()]) + context.vaccine_df.loc[0, [ + "NHS_NUMBER", + "PERSON_FORENAME", + "PERSON_SURNAME", + "PERSON_GENDER_CODE", + "PERSON_DOB", + "PERSON_POSTCODE", + "ACTION_FLAG", + "UNIQUE_ID", + "UNIQUE_ID_URI" + ]] = [ + context.create_object.contained[1].identifier[0].value, + context.create_object.contained[1].name[0].given[0], + context.create_object.contained[1].name[0].family, + context.create_object.contained[1].gender, + context.create_object.contained[1].birthDate.replace("-", ""), + context.create_object.contained[1].address[0].postalCode, + "DELETE", + context.create_object.identifier[0].value, + context.create_object.identifier[0].system + ] + create_batch_file(context) + context.vaccine_df.loc[0, "IMMS_ID"] = context.ImmsID + +@then("The delta and imms event table will be populated with the correct data for api created event") +@given("The delta and imms event table will be populated with the correct data for api created event") +def validate_imms_delta_table_for_api_created_event(context): + validate_imms_event_table_by_operation(context, "created") + validate_imms_delta_table_by_ImmsID(context) + +@when("Delete above vaccination record is made through batch file upload with mandatory field missing") +def upload_batch_file_to_s3_for_update_with_mandatory_field_missing(context): + # Build base record + record = build_batch_file(context) + context.vaccine_df = pd.DataFrame([record.dict()]) + + base_fields = { + "NHS_NUMBER": context.create_object.contained[1].identifier[0].value, + "PERSON_FORENAME": context.create_object.contained[1].name[0].given[0], + "PERSON_SURNAME": context.create_object.contained[1].name[0].family, + "PERSON_GENDER_CODE": context.create_object.contained[1].gender, + "PERSON_DOB": "", + "PERSON_POSTCODE": context.create_object.contained[1].address[0].postalCode, + "ACTION_FLAG": "DELETE", + "UNIQUE_ID": context.create_object.identifier[0].value, + "UNIQUE_ID_URI": context.create_object.identifier[0].system, + } + context.vaccine_df.loc[0, list(base_fields.keys())] = list(base_fields.values()) + + create_batch_file(context) + context.vaccine_df.loc[0, "IMMS_ID"] = context.ImmsID + + +@then("The imms event table status will be updated to delete and no change to record detail") +def validate_imms_event_table_for_delete_event(context): + validate_imms_event_table_by_operation(context, "deleted") + +@then("The delta table will have delete entry with no change to record detail") +def validate_delta_table_for_delete_event(context): + validate_imms_delta_table_by_deleted_ImmsID(context) \ No newline at end of file diff --git a/features/batchTests/Steps/test_update_batch_steps.py b/features/batchTests/Steps/test_update_batch_steps.py new file mode 100644 index 000000000..44a0bd34d --- /dev/null +++ b/features/batchTests/Steps/test_update_batch_steps.py @@ -0,0 +1,220 @@ +from multiprocessing import context +from src.dynamoDB.dynamo_db_helper import * +from src.objectModels.api_immunization_builder import * +from src.objectModels.batch.batch_file_builder import * +from utilities.batch_S3_buckets import * +from utilities.batch_file_helper import * +from utilities.date_helper import * +from utilities.text_helper import get_text +from utilities.vaccination_constants import * +from pytest_bdd import scenarios, given, when, then, parsers +import pytest_check as check +from .batch_common_steps import * +from features.APITests.steps.common_steps import * +from features.APITests.steps.test_create_steps import validate_imms_delta_table_by_ImmsID +from features.APITests.steps.test_update_steps import validate_delta_table_for_updated_event + +scenarios('batchTests/update_batch.feature') + +@given("batch file is created for below data as full dataset and each record has a valid update record in the same file") +def valid_batch_file_is_created_with_details(datatable, context): + build_dataFrame_using_datatable(datatable, context) + df_new = context.vaccine_df.copy() + df_update = df_new.copy() + df_update[["ACTION_FLAG", "EXPIRY_DATE"]] = ["UPDATE", "20281231"] + context.vaccine_df = pd.concat([df_new, df_update], ignore_index=True) + context.expected_version = 2 + create_batch_file(context) + +@given("I have created a valid vaccination record through API") +def create_valid_vaccination_record_through_api(context): + validVaccinationRecordIsCreated(context) + print(f"Created Immunization record with ImmsID: {context.ImmsID}") + + + +@when("An update to above vaccination record is made through batch file upload") +def upload_batch_file_to_s3_for_update(context): + record = build_batch_file(context) + context.vaccine_df = pd.DataFrame([record.dict()]) + context.vaccine_df.loc[0, [ + "NHS_NUMBER", + "PERSON_FORENAME", + "PERSON_SURNAME", + "PERSON_GENDER_CODE", + "PERSON_DOB", + "PERSON_POSTCODE", + "ACTION_FLAG", + "UNIQUE_ID", + "UNIQUE_ID_URI" + ]] = [ + context.create_object.contained[1].identifier[0].value, + context.create_object.contained[1].name[0].given[0], + context.create_object.contained[1].name[0].family, + context.create_object.contained[1].gender, + context.create_object.contained[1].birthDate.replace("-", ""), + context.create_object.contained[1].address[0].postalCode, + "UPDATE", + context.create_object.identifier[0].value, + context.create_object.identifier[0].system + ] + context.expected_version = 2 + create_batch_file(context) + +@then("The delta and imms event table will be populated with the correct data for api created event") +@given("The delta and imms event table will be populated with the correct data for api created event") +def validate_imms_delta_table_for_api_created_event(context): + validate_imms_event_table_by_operation(context, "created") + validate_imms_delta_table_by_ImmsID(context) + +@when("Send a update for Immunization event created with vaccination detail being updated through API request") +def send_update_for_immunization_event_with_vaccination_detail_updated(context): + valid_json_payload_is_created(context) + row = context.vaccine_df.loc[0] + context.immunization_object.contained[1].identifier[0].value = row["NHS_NUMBER"] + context.immunization_object.contained[1].name[0].given[0] = row["PERSON_FORENAME"] + context.immunization_object.contained[1].name[0].family = row["PERSON_SURNAME"] + reverse_gender_map = {v.value: v.name for v in GenderCode} + code = row["PERSON_GENDER_CODE"] + context.immunization_object.contained[1].gender = reverse_gender_map.get(code, "unknown") + context.immunization_object.contained[1].birthDate = ( + f"{row['PERSON_DOB'][:4]}-{row['PERSON_DOB'][4:6]}-{row['PERSON_DOB'][6:]}" + ) + context.immunization_object.contained[1].address[0].postalCode = row["PERSON_POSTCODE"] + context.immunization_object.identifier[0].value = row["UNIQUE_ID"] + context.immunization_object.identifier[0].system = row["UNIQUE_ID_URI"] + send_update_for_immunization_event(context) + +@then("Api request will be successful and tables will be updated correctly") +def api_request_will_be_successful_and_tables_will_be_updated_correctly(context): + The_request_will_have_status_code(context, 200) + validate_etag_in_header(context) + validate_imms_event_table_by_operation(context, "updated") + validate_delta_table_for_updated_event(context) + +@when("Update to above vaccination record is made through batch file upload with mandatory field missing") +def upload_batch_file_to_s3_for_update_with_mandatory_field_missing(context): + # Build base record + record = build_batch_file(context) + context.vaccine_df = pd.DataFrame([record.dict()]) + + base_fields = { + "NHS_NUMBER": context.create_object.contained[1].identifier[0].value, + "PERSON_FORENAME": context.create_object.contained[1].name[0].given[0], + "PERSON_SURNAME": context.create_object.contained[1].name[0].family, + "PERSON_GENDER_CODE": context.create_object.contained[1].gender, + "PERSON_DOB": context.create_object.contained[1].birthDate.replace("-", ""), + "PERSON_POSTCODE": context.create_object.contained[1].address[0].postalCode, + "ACTION_FLAG": "UPDATE", + "UNIQUE_ID": context.create_object.identifier[0].value, + "UNIQUE_ID_URI": context.create_object.identifier[0].system, + } + + context.vaccine_df.loc[0, list(base_fields.keys())] = list(base_fields.values()) + + context.vaccine_df = pd.concat([context.vaccine_df.loc[[0]]] * 19, ignore_index=True) + + missing_cases = { + 0: {"SITE_CODE": "", "PERSON_SURNAME": "empty_site_code"}, + 1: {"SITE_CODE_TYPE_URI": "", "PERSON_SURNAME": "empty_site_code_uri"}, + 2: {"LOCATION_CODE": "", "PERSON_SURNAME": "empty_location_code"}, + 3: {"LOCATION_CODE_TYPE_URI": "", "PERSON_SURNAME": "empty_location_code_uri"}, + 4: {"UNIQUE_ID": "", "PERSON_SURNAME": "no_unique_identifiers"}, + 5: {"UNIQUE_ID_URI": "", "PERSON_SURNAME": "no_unique_identifiers"}, + 6: {"PRIMARY_SOURCE": "", "PERSON_SURNAME": "empty_primary_source"}, + 7: {"VACCINATION_PROCEDURE_CODE": "", "PERSON_SURNAME": "no_procedure_code"}, + 8: {"SITE_CODE": " ", "PERSON_SURNAME": "no_site_code"}, + 9: {"SITE_CODE_TYPE_URI": " ", "PERSON_SURNAME": "no_site_code_uri"}, + 10: {"LOCATION_CODE": " ", "PERSON_SURNAME": "no_location_code"}, + 11: {"LOCATION_CODE_TYPE_URI": " ", "PERSON_SURNAME": "no_location_code_uri"}, + 12: {"UNIQUE_ID": " ", "PERSON_SURNAME": "no_unique_id"}, + 13: {"UNIQUE_ID_URI": " ", "PERSON_SURNAME": "no_unique_id_uri"}, + 14: {"PRIMARY_SOURCE": " ", "PERSON_SURNAME": "no_primary_source"}, + 15: {"VACCINATION_PROCEDURE_CODE": " ", "PERSON_SURNAME": "empty_procedure_code"}, + 16: {"PRIMARY_SOURCE": "test", "PERSON_SURNAME": "no_primary_source"}, + 17: {"ACTION_FLAG": "", "PERSON_SURNAME": "invalid_action_flag"}, + 18: {"ACTION_FLAG": " ", "PERSON_SURNAME": "invalid_action_flag"} + } + + # Apply all missing-field modifications + for row_idx, updates in missing_cases.items(): + for col, value in updates.items(): + context.vaccine_df.loc[row_idx, col] = value + + create_batch_file(context) + +@then("bus ack will have error records for all the updated records in the batch file") +def all_records_are_processed_successfully_in_the_batch_file(context): + file_rows = read_and_validate_bus_ack_file_content(context, False, True) + all_valid = validate_bus_ack_file_for_error_by_surname(context, file_rows) + assert all_valid, "One or more records failed validation checks" + +def validate_bus_ack_file_for_error_by_surname(context, file_rows) -> bool: + if not file_rows: + print("No rows found in BUS ACK file for failed records") + return False + + overall_valid = True + + for batch_idx, row in context.vaccine_df.iterrows(): + + bus_ack_row_number = batch_idx + 2 + + row_data_list = file_rows.get(bus_ack_row_number) + + if not row_data_list: + print(f"Batch row {batch_idx}: No BUS ACK entry found for row number {bus_ack_row_number}") + overall_valid = False + continue + + surname = str(row.get("PERSON_SURNAME", "")).strip() + expected_error = surname + expected_diagnostic = ERROR_MAP.get(expected_error, {}).get("diagnostics") + + for row_data in row_data_list: + i = row_data["row"] + fields = row_data["fields"] + row_valid = True + + header_response_code = fields[1] + issue_severity = fields[2] + issue_code = fields[3] + response_code = fields[6] + response_display = fields[7] + imms_id = fields[11] + operation_outcome = fields[12] + message_delivery = fields[13] + + + if header_response_code != "Fatal Error": + print(f"Row {i}: HEADER_RESPONSE_CODE is not 'Fatal Error'") + row_valid = False + if issue_severity != "Fatal": + print(f"Row {i}: ISSUE_SEVERITY is not 'Fatal'") + row_valid = False + if issue_code != "Fatal Error": + print(f"Row {i}: ISSUE_CODE is not 'Fatal Error'") + row_valid = False + if response_code != "30002": + print(f"Row {i}: RESPONSE_CODE is not '30002'") + row_valid = False + if response_display != "Business Level Response Value - Processing Error": + print(f"Row {i}: RESPONSE_DISPLAY is not expected value") + row_valid = False + if imms_id: + print(f"Row {i}: IMMS_ID should be null but is populated") + row_valid = False + if message_delivery != "False": + print(f"Row {i}: MESSAGE_DELIVERY is not 'False'") + row_valid = False + + if operation_outcome != expected_diagnostic: + print( + f"Row {i}: operation_outcome '{operation_outcome}' does not match " + f"expected diagnostics '{expected_diagnostic}' for surname '{expected_error}'" + ) + row_valid = False + + overall_valid = overall_valid and row_valid + + return overall_valid \ No newline at end of file diff --git a/features/batchTests/batch_file_validation.feature b/features/batchTests/batch_file_validation.feature index dd346ad3c..ccd80ecf8 100644 --- a/features/batchTests/batch_file_validation.feature +++ b/features/batchTests/batch_file_validation.feature @@ -29,7 +29,7 @@ Scenario: verify that vaccination file will be rejected if the processed file is Then file will be moved to destination bucket and inf ack file will be created And inf ack file has success status for processed batch file And bus ack file will be created - And bus ack will be empty as all records are processed successfully + And bus ack will not have any entry of successfully processed records And Audit table will have correct status, queue name and record count for the processed batch file When same batch file is uploaded again in s3 bucket Then file will be moved to destination bucket and inf ack file will be created for duplicate batch file upload @@ -79,7 +79,7 @@ Scenario: verify that vaccination file will be rejected if file delimiter is inv And bus ack file will not be created And Audit table will have 'Failed', 'SONAR_FLU' and 'File headers are invalid.' for the processed batch file -@vaccine_type_3in1 @supplier_name_TPP +@vaccine_type_3IN1 @supplier_name_TPP Scenario: verify that vaccination file will be rejected if one of the column name is invalid Given batch file is created with invalid column name for patient surname for below data | patient_id | unique_id | @@ -90,7 +90,7 @@ Scenario: verify that vaccination file will be rejected if one of the column nam And bus ack file will not be created And Audit table will have 'Failed', 'TPP_3IN1' and 'File headers are invalid.' for the processed batch file -@vaccine_type_3in1 @supplier_name_EMIS +@vaccine_type_3IN1 @supplier_name_EMIS Scenario: verify that vaccination file will be rejected if additional column is present Given batch file is created with additional column person age for below data | patient_id | unique_id | diff --git a/features/batchTests/create_batch.feature b/features/batchTests/create_batch.feature index 3424c0d70..1b5134d88 100644 --- a/features/batchTests/create_batch.feature +++ b/features/batchTests/create_batch.feature @@ -15,10 +15,10 @@ Scenario: Verify that full dataset vaccination record will be created through ba Then file will be moved to destination bucket and inf ack file will be created And inf ack file has success status for processed batch file And bus ack file will be created - And bus ack will be empty as all records are processed successfully + And bus ack will not have any entry of successfully processed records And Audit table will have correct status, queue name and record count for the processed batch file And The imms event table will be populated with the correct data for 'created' event for records in batch file - And The delta table will be populated with the correct data for all records in batch file + And The delta table will be populated with the correct data for all created records in batch file @smoke @delete_cleanup_batch @vaccine_type_MMR @supplier_name_TPP @@ -34,10 +34,10 @@ Scenario: Verify that minimum dataset vaccination record will be created through Then file will be moved to destination bucket and inf ack file will be created And inf ack file has success status for processed batch file And bus ack file will be created - And bus ack will be empty as all records are processed successfully + And bus ack will not have any entry of successfully processed records And Audit table will have correct status, queue name and record count for the processed batch file And The imms event table will be populated with the correct data for 'created' event for records in batch file - And The delta table will be populated with the correct data for all records in batch file + And The delta table will be populated with the correct data for all created records in batch file @vaccine_type_FLU @supplier_name_MAVIS Scenario: Verify that vaccination record will be get rejected if date_and_time is invalid in batch file @@ -54,7 +54,7 @@ Scenario: Verify that vaccination record will be get rejected if date_and_time i And all records are rejected in the bus ack file and no imms id is generated And Audit table will have correct status, queue name and record count for the processed batch file -@vaccine_type_FLU @supplier_name_MAVIS +@vaccine_type_6IN1 @supplier_name_EMIS Scenario: verify that vaccination record will be get rejected if recorded_date is invalid in batch file Given batch file is created for below data where recorded field has invalid date | patient_id | unique_id | @@ -69,7 +69,7 @@ Scenario: verify that vaccination record will be get rejected if recorded_date i And all records are rejected in the bus ack file and no imms id is generated And Audit table will have correct status, queue name and record count for the processed batch file -@vaccine_type_FLU @supplier_name_MAVIS +@vaccine_type_4IN1 @supplier_name_TPP Scenario: verify that vaccination record will be get rejected if expiry_date is invalid in batch file Given batch file is created for below data where expiry field has invalid date | patient_id | unique_id | @@ -120,7 +120,7 @@ Scenario: verify that vaccination record will be get rejected if Person nhs numb And all records are rejected in the bus ack file and no imms id is generated And Audit table will have correct status, queue name and record count for the processed batch file -@vaccine_type_FLU @supplier_name_MAVIS +@vaccine_type_BCG @supplier_name_TPP Scenario: verify that vaccination record will be get successful if performer is invalid in batch file Given batch file is created for below data where performer detail has invalid data | patient_id | unique_id | @@ -130,12 +130,12 @@ Scenario: verify that vaccination record will be get successful if performer is Then file will be moved to destination bucket and inf ack file will be created And inf ack file has success status for processed batch file And bus ack file will be created - And bus ack will be empty as all records are processed successfully + And bus ack will not have any entry of successfully processed records And Audit table will have correct status, queue name and record count for the processed batch file And The imms event table will be populated with the correct data for 'created' event for records in batch file - And The delta table will be populated with the correct data for all records in batch file + And The delta table will be populated with the correct data for all created records in batch file -@vaccine_type_FLU @supplier_name_SONAR +@vaccine_type_ROTAVIRUS @supplier_name_TPP Scenario: verify that vaccination record will be get successful with different valid value in gender field Given batch file is created for below data where person detail has valid values | patient_id | unique_id | @@ -154,10 +154,10 @@ Scenario: verify that vaccination record will be get successful with different v Then file will be moved to destination bucket and inf ack file will be created And inf ack file has success status for processed batch file And bus ack file will be created - And bus ack will be empty as all records are processed successfully + And bus ack will not have any entry of successfully processed records And Audit table will have correct status, queue name and record count for the processed batch file And The imms event table will be populated with the correct data for 'created' event for records in batch file - And The delta table will be populated with the correct data for all records in batch file + And The delta table will be populated with the correct data for all created records in batch file @vaccine_type_FLU @supplier_name_MAVIS Scenario: verify that vaccination record will be get rejected if mandatory fields for site, location and unique identifiers are missing in batch file @@ -189,7 +189,7 @@ Scenario: verify that vaccination record will be get rejected if mandatory field And all records are rejected in the bus ack file and no imms id is generated And Audit table will have correct status, queue name and record count for the processed batch file -@delete_cleanup_batch @vaccine_type_COVID @supplier_name_EMIS +@delete_cleanup_batch @vaccine_type_HIB @supplier_name_EMIS Scenario: verify that vaccination record will be successful if mandatory field for site, location and unique URI are invalid in batch file Given batch file is created for below data where mandatory field for site, location and unique uri values are invalid | patient_id | unique_id | @@ -200,10 +200,10 @@ Scenario: verify that vaccination record will be successful if mandatory field f Then file will be moved to destination bucket and inf ack file will be created And inf ack file has success status for processed batch file And bus ack file will be created - And bus ack will be empty as all records are processed successfully + And bus ack will not have any entry of successfully processed records And Audit table will have correct status, queue name and record count for the processed batch file And The imms event table will be populated with the correct data for 'created' event for records in batch file - And The delta table will be populated with the correct data for all records in batch file + And The delta table will be populated with the correct data for all created records in batch file @delete_cleanup_batch @vaccine_type_MENACWY @supplier_name_TPP @@ -218,13 +218,13 @@ Scenario: verify that vaccination record will be get successful if action flag h Then file will be moved to destination bucket and inf ack file will be created And inf ack file has success status for processed batch file And bus ack file will be created - And bus ack will be empty as all records are processed successfully + And bus ack will not have any entry of successfully processed records And Audit table will have correct status, queue name and record count for the processed batch file And The imms event table will be populated with the correct data for 'created' event for records in batch file - And The delta table will be populated with the correct data for all records in batch file + And The delta table will be populated with the correct data for all created records in batch file -@vaccine_type_3in1 @supplier_name_TPP +@vaccine_type_3IN1 @supplier_name_TPP Scenario: verify that vaccination record will be get rejected if non mandatory fields are empty string in batch file Given batch file is created for below data where non mandatory fields are empty string | patient_id | unique_id | @@ -249,7 +249,7 @@ Scenario: verify that vaccination record will be get rejected if non mandatory f And all records are rejected in the bus ack file and no imms id is generated And Audit table will have correct status, queue name and record count for the processed batch file -@delete_cleanup_batch @vaccine_type_3in1 @supplier_name_TPP +@delete_cleanup_batch @vaccine_type_3IN1 @supplier_name_TPP Scenario: verify that vaccination record will be get successful if non mandatory fields are missing in batch file Given batch file is created for below data where non mandatory fields are missing | patient_id | unique_id | @@ -271,7 +271,7 @@ Scenario: verify that vaccination record will be get successful if non mandatory Then file will be moved to destination bucket and inf ack file will be created And inf ack file has success status for processed batch file And bus ack file will be created - And bus ack will be empty as all records are processed successfully + And bus ack will not have any entry of successfully processed records And Audit table will have correct status, queue name and record count for the processed batch file And The imms event table will be populated with the correct data for 'created' event for records in batch file - And The delta table will be populated with the correct data for all records in batch file + And The delta table will be populated with the correct data for all created records in batch file diff --git a/features/batchTests/delete_batch.feature b/features/batchTests/delete_batch.feature new file mode 100644 index 000000000..53b19a8b4 --- /dev/null +++ b/features/batchTests/delete_batch.feature @@ -0,0 +1,48 @@ +@Delete_Batch_Feature @functional +Feature: Create the immunization event for a patient through batch file and update the record from batch or Api calls + +@smoke +@vaccine_type_BCG @supplier_name_TPP +Scenario: Delete immunization event for a patient through batch file + Given batch file is created for below data as full dataset and each record has a valid delete record in the same file + | patient_id | unique_id | + | Random | Valid_NhsNumber | + | InvalidInPDS | InvalidInPDS_NhsNumber| + | SFlag | SFlag_NhsNumber | + | Mod11_NHS | Mod11_NhSNumber | + | OldNHSNo | OldNHSNo | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'deleted' event for records in batch file + And The delta table will be populated with the correct data for all created records in batch file + And The delta table will be populated with the correct data for all deleted records in batch file + +@vaccine_type_MENB @patient_id_Random @supplier_name_EMIS +Scenario: Verify that the API vaccination record will be successful deleted by batch file upload + Given I have created a valid vaccination record through API + And The delta and imms event table will be populated with the correct data for api created event + When An delete to above vaccination record is made through batch file upload + And batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table status will be updated to delete and no change to record detail + And The delta table will have delete entry with no change to record detail + +@vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +Scenario: Verify that the API vaccination record will be successful deleted and batch file will successful with mandatory field missing + Given I have created a valid vaccination record through API + When Delete above vaccination record is made through batch file upload with mandatory field missing + And batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And The imms event table status will be updated to delete and no change to record detail + And The delta table will have delete entry with no change to record detail diff --git a/features/batchTests/update_batch.feature b/features/batchTests/update_batch.feature new file mode 100644 index 000000000..4b819ae3b --- /dev/null +++ b/features/batchTests/update_batch.feature @@ -0,0 +1,63 @@ +@Update_Batch_Feature @functional +Feature: Create the immunization event for a patient through batch file and update the record from batch or Api calls + +@smoke +@delete_cleanup_batch @vaccine_type_MMR @supplier_name_TPP +Scenario: Update immunization event for a patient through batch file + Given batch file is created for below data as full dataset and each record has a valid update record in the same file + | patient_id | unique_id | + | Random | Valid_NhsNumber | + | InvalidInPDS | InvalidInPDS_NhsNumber| + | SFlag | SFlag_NhsNumber | + | Mod11_NHS | Mod11_NhSNumber | + | OldNHSNo | OldNHSNo | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'updated' event for records in batch file + And The delta table will be populated with the correct data for all created records in batch file + And The delta table will be populated with the correct data for all updated records in batch file + +@Delete_cleanUp @vaccine_type_ROTAVIRUS @patient_id_Random @supplier_name_EMIS +Scenario: Verify that the API vaccination record will be successful updated by batch file upload + Given I have created a valid vaccination record through API + And The delta and imms event table will be populated with the correct data for api created event + When An update to above vaccination record is made through batch file upload + And batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'updated' event for records in batch file + And The delta table will be populated with the correct data for all updated records in batch file + +@Delete_cleanUp @vaccine_type_6IN1 @patient_id_Random @supplier_name_TPP +Scenario: Verify that the batch vaccination record will be successful updated by API request + Given batch file is created for below data as full dataset + | patient_id | unique_id | + | Random | Valid_NhsNumber | + When batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will not have any entry of successfully processed records + And Audit table will have correct status, queue name and record count for the processed batch file + And The imms event table will be populated with the correct data for 'created' event for records in batch file + And The delta table will be populated with the correct data for all created records in batch file + When Send a update for Immunization event created with vaccination detail being updated through API request + Then Api request will be successful and tables will be updated correctly + +@Delete_cleanUp @vaccine_type_RSV @patient_id_Random @supplier_name_RAVS +Scenario: Verify that the API vaccination record will be successful updated and batch file will fail upload due to mandatory field missing + Given I have created a valid vaccination record through API + When Update to above vaccination record is made through batch file upload with mandatory field missing + And batch file is uploaded in s3 bucket + Then file will be moved to destination bucket and inf ack file will be created + And inf ack file has success status for processed batch file + And bus ack file will be created + And bus ack will have error records for all the updated records in the batch file + And The delta and imms event table will be populated with the correct data for api created event diff --git a/pytest.ini b/pytest.ini index 8c34b32bb..2fc05a8b3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -33,7 +33,15 @@ markers = vaccine_type_MMR: tag for MMR vaccine type scenarios vaccine_type_MMRV: tag for MMRV vaccine type scenarios vaccine_type_MENACWY: tag for MENACWY vaccine type scenarios - vaccine_type_3in1: tag for 3in1 vaccine type scenarios + vaccine_type_3IN1: tag for 3IN1 vaccine type scenarios + vaccine_type_SHINGLES: tag for SHINGLES vaccine type scenarios + vaccine_type_HIB: tag for HIB vaccine type scenarios + vaccine_type_HEPB: tag for HEPB vaccine type scenarios + vaccine_type_6IN1: tag for 6IN1 vaccine type scenarios + vaccine_type_4IN1: tag for 4IN1 vaccine type scenarios + vaccine_type_MENB: tag for MENB vaccine type scenarios + vaccine_type_ROTAVIRUS: tag for ROTAVIRUS vaccine type scenarios + vaccine_type_BCG: tag for BCG vaccine type scenarios supplier_name_RAVS: tag for RAVS supplier name scenarios supplier_name_MAVIS: tag for MAVIS supplier name scenarios supplier_name_EMIS: tag for EMIS supplier name scenarios diff --git a/src/dynamoDB/dynamo_db_helper.py b/src/dynamoDB/dynamo_db_helper.py index fb87b7dcc..9f8293530 100644 --- a/src/dynamoDB/dynamo_db_helper.py +++ b/src/dynamoDB/dynamo_db_helper.py @@ -275,14 +275,14 @@ def validate_audit_table_record(context, item, expected_status: str, expected_er ) def validate_imms_delta_record_with_batch_record(context, batch_record, item, event_type, action_flag): - event = item[0].get("Imms") + event = item.get("Imms") assert event, "Imms field missing in items." fields_to_compare = [ - ("Operation", event_type.upper(), item[0].get("Operation")), - ("SupplierSystem", context.supplier_name.lower(), item[0].get("SupplierSystem").lower()), - ("VaccineType", f"{context.vaccine_type.lower()}", item[0].get("VaccineType").lower()), - ("Source", "IEDS", item[0].get("Source")), + ("Operation", event_type.upper(), item.get("Operation")), + ("SupplierSystem", context.supplier_name.lower(), item.get("SupplierSystem").lower()), + ("VaccineType", f"{context.vaccine_type.lower()}", item.get("VaccineType").lower()), + ("Source", "IEDS", item.get("Source")), ("CONVERSION_ERRORS", [], event.get("CONVERSION_ERRORS")), ("PERSON_FORENAME", batch_record["PERSON_FORENAME"], event.get("PERSON_FORENAME")), ("PERSON_SURNAME", batch_record["PERSON_SURNAME"], event.get("PERSON_SURNAME")), diff --git a/src/objectModels/batch/batch_file_builder.py b/src/objectModels/batch/batch_file_builder.py index 8bdfab1ff..6407b07c6 100644 --- a/src/objectModels/batch/batch_file_builder.py +++ b/src/objectModels/batch/batch_file_builder.py @@ -154,7 +154,7 @@ def save_record_to_batch_files_directory(context, delimiter): with open(file_path, mode="w", newline="", encoding="utf-8") as file: writer = csv.writer(file, delimiter=delimiter, quoting=csv.QUOTE_ALL) - writer.writerow(df.columns.tolist()) # Write header + writer.writerow(df.columns.tolist()) for row in df.itertuples(index=False): writer.writerow(row) diff --git a/utilities/batch_file_helper.py b/utilities/batch_file_helper.py index e9b84e51a..bf140ac26 100644 --- a/utilities/batch_file_helper.py +++ b/utilities/batch_file_helper.py @@ -184,8 +184,16 @@ def validate_bus_ack_file_for_error(context, file_rows) -> bool: return overall_valid +def read_and_validate_bus_ack_file_content( + context, + by_local_id: bool = True, + by_row_number: bool = False +) -> dict: + + # Prevent invalid combinations + if by_local_id and by_row_number: + raise ValueError("Choose only one mode: by_local_id OR by_row_number") -def read_and_validate_bus_ack_file_content(context): content = context.fileContent.strip() lines = content.split("\n") @@ -211,30 +219,37 @@ def read_and_validate_bus_ack_file_content(context): return {} header = lines[0].split("|") - - if len(header) != len(expected_header): - print(f"Header column count mismatch: expected {len(expected_header)}, got {len(header)}") - return {} - if header != expected_header: - print(f"Header names mismatch.\nExpected: {expected_header}\nGot: {header}") + print("Header mismatch") return {} - - file_rows = {} - if len(lines) > 1: + + file_rows = {} + + if by_local_id: + for i, line in enumerate(lines[1:], start=2): + fields = line.split("|") + local_id = normalize_for_lookup(fields[10]) + + file_rows.setdefault(local_id, []).append( + { + "row": i, + "fields": fields, + "original_local_id": fields[10], + } + ) + return file_rows + + if by_row_number: for i, line in enumerate(lines[1:], start=2): fields = line.split("|") - if len(fields) < len(expected_header): - print(f"Row {i} has insufficient columns: {fields}") - continue - local_id = fields[10] - normalized_id = normalize_for_lookup(local_id) - entry = { - "row": i, - "fields": fields, - "original_local_id": local_id, - } - file_rows.setdefault(normalized_id, []).append(entry) - - return file_rows \ No newline at end of file + file_rows[i] = [ + { + "row": i, + "fields": fields, + "original_local_id": fields[10], + } + ] + return file_rows + + raise ValueError("You must select either by_local_id=True or by_row_number=True") \ No newline at end of file diff --git a/utilities/context.py b/utilities/context.py index 55e021dc3..a80b5256e 100644 --- a/utilities/context.py +++ b/utilities/context.py @@ -42,4 +42,5 @@ def __init__(self): self.supplier_ods_code = None self.working_directory = None self.fileContent = None + self.delta_cache = None diff --git a/utilities/enums.py b/utilities/enums.py index a69bfd1d8..d0a78a1e6 100644 --- a/utilities/enums.py +++ b/utilities/enums.py @@ -26,3 +26,20 @@ class GenderCode(Enum): female = "2" unknown = "0" other = "9" + +class ActionMap(Enum): + new = (Operation.created, ActionFlag.created) + update = (Operation.updated, ActionFlag.updated) + delete = (Operation.deleted, ActionFlag.deleted) + created = (Operation.created, ActionFlag.created) + updated = (Operation.updated, ActionFlag.updated) + deleted = (Operation.deleted, ActionFlag.deleted) + + @property + def operation(self): + return self.value[0] + + @property + def action_flag(self): + return self.value[1] + diff --git a/utilities/error_constants.py b/utilities/error_constants.py index f028e8102..fa36d5579 100644 --- a/utilities/error_constants.py +++ b/utilities/error_constants.py @@ -31,7 +31,7 @@ }, "invalid_DiseaseType": { "code": "INVALID", - "diagnostics": "-immunization.target must be one or more of the following: RSV, SHINGLES, MMR, MMRV, MENACWY, COVID, PNEUMOCOCCAL, FLU, HPV, PERTUSSIS, 3IN1" + "diagnostics": "-immunization.target must be one or more of the following: ROTAVIRUS, RSV, SHINGLES, 6IN1, MMR, FLU, 3IN1, PERTUSSIS, MENB, HIB, MMRV, BCG, MENACWY, 4IN1, COVID, PNEUMOCOCCAL, HPV, HEPB" }, "invalid_DateFrom": { "code": "INVALID", diff --git a/utilities/vaccination_constants.py b/utilities/vaccination_constants.py index b0a3471f2..6484a0607 100644 --- a/utilities/vaccination_constants.py +++ b/utilities/vaccination_constants.py @@ -216,10 +216,133 @@ "manufacturer": "Pfizer Ltd" } ], + "BCG":[ + { + "system": "http://snomed.info/sct", + "code": "37240111000001101", + "display": "BCG Vaccine AJV powder for suspension for injection 1ml vials (AJ Vaccines)", + "stringValue": "BCG Vaccine AJV powder for suspension for injection 1ml vials", + "idValue": "37240111000001101", + "manufacturer": "AJ Vaccines" + }, + { + "system": "http://snomed.info/sct", + "code": "9316511000001100", + "display": "BCG vaccine powder and solvent for suspension for injection vials 10 vial (Pfizer Ltd)", + "stringValue": "BCG vaccine powder and solvent for suspension for injection vials 10 vial", + "idValue": "9316511000001100", + "manufacturer": "Pfizer Ltd" + } + ], + "HEPB":[ + { + "system": "http://snomed.info/sct", + "code": "10752011000001102", + "display": "HBVAXPRO 10micrograms/1ml vaccine suspension for injection pre-filled syringes (Merck Sharp & Dohme (UK) Ltd)", + "stringValue": "HBVAXPRO 10micrograms/1ml vaccine suspension for injection pre-filled syringes", + "idValue": "107520110001102", + "manufacturer": "Merck Sharp & Dohme (UK) Ltd" + }, + { + "system": "http://snomed.info/sct", + "code": "871822003", + "display": "Vaccine product containing only Hepatitis B virus antigen (Pfizer Ltd)", + "stringValue": "Vaccine product containing only Hepatitis B virus antigen", + "idValue": "871822003", + "manufacturer": "Pfizer Ltd" + } + ], + "HIB":[ + { + "system": "http://snomed.info/sct", + "code": "9903711000001109", + "display": "Menitorix powder and solvent for solution for injection 0.5ml vials (GlaxoSmithKline)", + "stringValue": "Haemophilus type b / Meningococcal C conjugate vaccine powder and solvent for solution for injection 0.5ml vials 1 vial", + "idValue": "99037110001109", + "manufacturer": "GlaxoSmithKline UK Ltd" + }, + { + "system": "http://snomed.info/sct", + "code": "9903611000001100", + "display": "Haemophilus type b / Meningococcal C conjugate vaccine powder and solvent for solution for injection 0.5ml vials 1 vial", + "stringValue": "Menitorix powder and solvent for solution for injection 0.5ml vials (GlaxoSmithKline)", + "idValue": "99036110001100", + "manufacturer": "Sanofi" + } + ], + "MENB":[ + { + "system": "http://snomed.info/sct", + "code": "23584211000001109", + "display": "Bexsero vaccine suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd)", + "stringValue": "Bexsero vaccine powder and solvent for solution for injection 0.5ml vials", + "idValue": "235842110001109", + "manufacturer": "GlaxoSmithKline UK Ltd" + }, + { + "system": "http://snomed.info/sct", + "code": "37430711000001103", + "display": "Bexsero vaccine suspension for injection 0.5ml pre-filled syringes (CST Pharma Ltd) 1 pre-filled disposable injection", + "stringValue": "Bexsero vaccine suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd)", + "idValue": "374307110001103", + "manufacturer": "Pfizer Ltd" + } + ], + "ROTAVIRUS":[ + { + "system": "http://snomed.info/sct", + "code": "34609911000001106", + "display": "Rotarix vaccine live oral suspension 1.5ml tube (GlaxoSmithKline UK Ltd)", + "stringValue": "Rotarix oral vaccine suspension for oral administration 1.5ml pre-filled syringes", + "idValue": "34609911000001106", + "manufacturer": "GlaxoSmithKline UK Ltd" + }, + { + "system": "http://snomed.info/sct", + "code": "17996111000001109", + "display": "Rotavirus vaccine live oral suspension 1.5ml pre-filled syringes", + "stringValue": "Rotarix vaccine live oral suspension 1.5ml tube (GlaxoSmithKline UK Ltd)", + "idValue": "17996111000001109", + "manufacturer": "Merck Sharp & Dohme (UK) Ltd" + } + ], + "4IN1":[ + { + "system": "http://snomed.info/sct", + "code": "26267211000001100", + "display": "Boostrix-IPV suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd)", + "stringValue": "Vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and inactivated whole Human poliovirus antigens", + "idValue": "1303503001", + "manufacturer": "GlaxoSmithKline UK Ltd" + }, + { + "system": "http://snomed.info/sct", + "code": "871893003", + "display": "Vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and inactivated whole Human poliovirus antigens", + "stringValue": "Boostrix-IPV suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd)", + "idValue": "1303503001", + "manufacturer": "Pfizer Ltd" + } + ], + "6IN1":[ + { + "system": "http://snomed.info/sct", + "code": "34765811000001105", + "display": "Infanrix Hexa vaccine powder and suspension for suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd) ", + "stringValue": "Vaccine product containing only Clostridium tetani and Corynebacterium diphtheriae and inactivated Human poliovirus and acellular Bordetella pertussis and Haemophilus influenzae type b and Hepatitis B virus antigens", + "idValue": "347658110001105", + "manufacturer": "GlaxoSmithKline UK Ltd" + }, + { + "system": "http://snomed.info/sct", + "code": "1162634005", + "display": "Pediatric vaccine product containing only acellular Bordetella pertussis, Clostridium tetani and Corynebacterium diphtheriae toxoids, Haemophilus influenzae type b conjugated, Hepatitis B virus and inactivated Human poliovirus antigens", + "stringValue": "Infanrix Hexa vaccine powder and suspension for suspension for injection 0.5ml pre-filled syringes (GlaxoSmithKline UK Ltd)", + "idValue": "347658110001105", + "manufacturer": "GlaxoSmithKline UK Ltd" + } + ] } - - - VACCINATION_PROCEDURE_MAP = { "COVID": [ { @@ -365,7 +488,7 @@ "idValue": "8662002" } ], - "PERTUSSIS": [ + "PERTUSSIS": [ { "system": "http://snomed.info/sct", "code": "956951000000104", @@ -374,7 +497,7 @@ "idValue": "1000000104" }, ], - "PNEUMOCOCCAL": [ + "PNEUMOCOCCAL": [ { "system": "http://snomed.info/sct", "code": "722215002", @@ -389,8 +512,122 @@ "stringValue": "First pneumococcal conjugated", "idValue": "4326365" } + ], + "BCG": [ + { + "system": "http://snomed.info/sct", + "code": "42284007", + "display": "Administration of vaccine product containing only live attenuated Mycobacterium bovis antigen", + "stringValue": "Requires Bacillus Calmette-Guerin vaccination", + "idValue": "4326365" + }, + { + "system": "http://snomed.info/sct", + "code": "429069001", + "display": "Requires Bacillus Calmette-Guerin vaccination", + "stringValue": "Administration of vaccine product containing only live attenuated Mycobacterium bovis antigen", + "idValue": "4326365" + } + ], + "HEPB": [ + { + "system": "http://snomed.info/sct", + "code": "170370000", + "display": "Administration of first dose of vaccine product containing only Hepatitis B virus antigen", + "stringValue": "Administration of booster dose of vaccine product containing only Hepatitis B virus antigen", + "idValue": "4326365" + }, + { + "system": "http://snomed.info/sct", + "code": "170373003", + "display": "Administration of booster dose of vaccine product containing only Hepatitis B virus antigen", + "stringValue": "Administration of first dose of vaccine product containing only Hepatitis B virus antigen", + "idValue": "4326365" + } + ], + "HIB": [ + { + "system": "http://snomed.info/sct", + "code": "428975001", + "display": "Haemophilus influenzae type B Meningitis C (HibMenC) vaccination codes", + "stringValue": "Haemophilus influenzae type B Meningitis C (HibMenC) vaccination codes", + "idValue": "4326365" + }, + { + "system": "http://snomed.info/sct", + "code": "712833000", + "display": "Haemophilus influenzae type B Meningitis C (HibMenC) vaccination codes", + "stringValue": "Haemophilus influenzae type B Meningitis C (HibMenC) vaccination codes", + "idValue": "4326365" + } + ], + "MENB": [ + { + "system": "http://snomed.info/sct", + "code": "720539004", + "display": "Administration of first dose of vaccine product containing only Neisseria meningitidis serogroup B antigen", + "stringValue": "Recombinant meningococcal group B and outer membrane vesicle vaccination", + "idValue": "235842110001109", + "manufacturer": "GlaxoSmithKline UK Ltd" + }, + { + "system": "http://snomed.info/sct", + "code": "516301000000101", + "display": "Recombinant meningococcal group B and outer membrane vesicle vaccination", + "stringValue": "Administration of first dose of vaccine product containing only Neisseria meningitidis serogroup B antigen", + "idValue": "516301000101", + "manufacturer": "CST Pharma Ltd" + } + ], + "ROTAVIRUS": [ + { + "system": "http://snomed.info/sct", + "code": "868631000000102", + "display": "First rotavirus vaccination", + "stringValue": "Administration of vaccine product containing only Rotavirus antigen", + "idValue": "4326365" + }, + { + "system": "http://snomed.info/sct", + "code": "415354003", + "display": "Administration of vaccine product containing only Rotavirus antigen", + "stringValue": "First rotavirus vaccination", + "idValue": "4326365" + } + ], + "4IN1": [ + { + "system": "http://snomed.info/sct", + "code": "868273007", + "display": "Administration of vaccine product containing only Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and Human poliovirus antigens", + "stringValue": "Administration of vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and inactivated whole Human poliovirus antigens", + "idValue": "1303503001" + }, + { + "system": "http://snomed.info/sct", + "code": "247821000000102", + "display": "Booster diphtheria, tetanus, acellular pertussis and inactivated polio vaccination", + "stringValue": "Administration of vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and inactivated whole Human poliovirus antigens", + "idValue": "1303503001" + } + ], + "6IN1": [ + { + "system": "http://snomed.info/sct", + "code": "1082441000000108", + "display": "First diphtheria, tetanus and acellular pertussis, inactivated polio, Haemophilus influenzae type b and hepatitis B vaccination", + "stringValue": "Administration of vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and inactivated whole Human poliovirus antigens", + "idValue": "1303503001" + }, + { + "system": "http://snomed.info/sct", + "code": "1162640003", + "display": "Administration of vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and Haemophilus influenzae type b and Hepatitis B virus and inactivated Human poliovirus antigens", + "stringValue": "Administration of vaccine product containing only acellular Bordetella pertussis and Clostridium tetani and Corynebacterium diphtheriae and inactivated whole Human poliovirus antigens", + "idValue": "1303503001" + } ] - + } SITE_MAP = [ @@ -563,5 +800,95 @@ "code": "23511006", "display": "Meningococcal infectious disease" } - ] -} \ No newline at end of file + ], + "4IN1": [ + { + "system": "http://snomed.info/sct", + "code": "398102009", + "display": "Acute poliomyelitis" + }, + { + "system": "http://snomed.info/sct", + "code": "397430003", + "display": "Diphtheria caused by Corynebacterium diphtheriae" + }, + { + "system": "http://snomed.info/sct", + "code": "27836007", + "display": "Pertussis" + }, + { + "system": "http://snomed.info/sct", + "code": "76902006", + "display": "Tetanus" + } + ], + "6IN1": [ + { + "system": "http://snomed.info/sct", + "code": "398102009", + "display": "Acute poliomyelitis" + }, + { + "system": "http://snomed.info/sct", + "code": "397430003", + "display": "Diphtheria caused by Corynebacterium diphtheriae" + }, + { + "system": "http://snomed.info/sct", + "code": "709410003", + "display": "Haemophilus influenzae type b infection" + }, + { + "system": "http://snomed.info/sct", + "code": "27836007", + "display": "Pertussis" + }, + { + "system": "http://snomed.info/sct", + "code": "76902006", + "display": "Tetanus" + }, + { + "system": "http://snomed.info/sct", + "code": "66071002", + "display": "Type B viral hepatitis" + } + ], + "BCG": [ + { + "system": "http://snomed.info/sct", + "code": "56717001", + "display": "Tuberculosis" + } + ], + "HEPB": [ + { + "system": "http://snomed.info/sct", + "code": "66071002", + "display": "Type B viral hepatitis" + } + ], + "HIB": [ + { + "system": "http://snomed.info/sct", + "code": "709410003", + "display": "Haemophilus influenzae type b infection" + } + ], + "MENB": [ + { + "system": "http://snomed.info/sct", + "code": "1354584007", + "display": "Meningococcal infectious disease caused by Neisseria meningitidis serogroup B" + } + ], + "ROTAVIRUS": [ + { + "system": "http://snomed.info/sct", + "code": "186150001", + "display": "Enteritis caused by rotavirus" + } + ] + + } \ No newline at end of file