diff --git a/classes/repositories/general_repository.py b/classes/repositories/general_repository.py index fd2cda53..035bcdaf 100644 --- a/classes/repositories/general_repository.py +++ b/classes/repositories/general_repository.py @@ -67,3 +67,26 @@ def process_new_lynch_patients(self): ) logging.debug("[END] process_new_lynch_patients") + + def run_lynch_invitations(self): + """ + Executes the pkg_lynch.p_invite_lynch_subjects stored procedure to send invitations to Lynch subjects. + Raises: + oracledb.DatabaseError: If there is an error executing the stored procedure. + """ + logging.debug("[START] run_lynch_invitations") + try: + self.oracle_db.execute_stored_procedure( + "pkg_lynch.p_invite_lynch_subjects", + in_params=None, + out_params=[oracledb.CURSOR], + ) + except Exception as e: + logging.error( + "Error executing pkg_lynch.p_invite_lynch_subjects", exc_info=True + ) + raise oracledb.DatabaseError( + f"Error executing pkg_lynch.p_invite_lynch_subjects: {e}" + ) + + logging.debug("[END] run_lynch_invitations") diff --git a/pages/screening_subject_search/subject_screening_summary_page.py b/pages/screening_subject_search/subject_screening_summary_page.py index 162ad6f8..2c0418d9 100644 --- a/pages/screening_subject_search/subject_screening_summary_page.py +++ b/pages/screening_subject_search/subject_screening_summary_page.py @@ -62,6 +62,9 @@ def __init__(self, page: Page): self.first_surveillance_episode_link = self.page.get_by_role( "link", name="Surveillance" ).first + self.first_lynch_surveillance_episode_link = self.page.get_by_role( + "link", name="Lynch Surveillance" + ).first self.datasets_link = self.page.get_by_role("link", name="Datasets") self.advance_fobt_screening_episode_button = self.page.get_by_role( "button", name="Advance FOBT Screening Episode" @@ -251,6 +254,10 @@ def click_first_surveillance_episode_link(self) -> None: """Click on the first Surveillance episode link.""" self.click(self.first_surveillance_episode_link) + def click_first_lynch_surveillance_episode_link(self) -> None: + """Click on the first Lynch Surveillance episode link.""" + self.click(self.first_lynch_surveillance_episode_link) + def click_datasets_link(self) -> None: """Click on the 'Datasets' link.""" self.click(self.datasets_link) diff --git a/pytest.ini b/pytest.ini index 78667463..5ccc82a8 100644 --- a/pytest.ini +++ b/pytest.ini @@ -57,3 +57,4 @@ markers = fobt_regression_tests: tests that are part of the fobt regression test suite lynch_self_referral_tests: tests that are part of the lynch self referral test suite surveillance_regression_tests: tests that are part of the surveillance regression test suite + lynch_regression_tests: tests that are part of the lynch regression test suite diff --git a/tests/regression/regression_tests/lynch_regression_tests/test_lynch_scenario_2.py b/tests/regression/regression_tests/lynch_regression_tests/test_lynch_scenario_2.py new file mode 100644 index 00000000..cad2f8b3 --- /dev/null +++ b/tests/regression/regression_tests/lynch_regression_tests/test_lynch_scenario_2.py @@ -0,0 +1,278 @@ +import logging +import pytest +from playwright.sync_api import Page +from classes.repositories.general_repository import GeneralRepository +from pages.base_page import BasePage +from pages.logout.log_out_page import LogoutPage +from pages.screening_practitioner_appointments.appointment_detail_page import ( + AppointmentDetailPage, +) +from pages.screening_subject_search.episode_events_and_notes_page import ( + EpisodeEventsAndNotesPage, +) +from pages.screening_subject_search.subject_screening_summary_page import ( + SubjectScreeningSummaryPage, +) +from utils import screening_subject_page_searcher +from utils.appointments import book_appointments +from utils.batch_processing import batch_processing +from utils.lynch_utils import LynchUtils +from utils.oracle.oracle import OracleDB +from utils.subject_assertion import subject_assertion +from utils.user_tools import UserTools + + +@pytest.mark.usefixtures("setup_org_and_appointments") +@pytest.mark.vpn_required +@pytest.mark.regression +@pytest.mark.lynch_regression_tests +def test_scenario_2(page: Page) -> None: + """ + Scenario: 2 - Screening Centre discharges patient prior to colonoscopy assessment + + G1-G2-G3-A183-A25-J24-J25-J26-C203 in age range [SSCL4b(J26)] + + This scenario tests where the screening centre discharges the patient before the colonoscopy assessment appointment takes place. + + Scenario summary: + + > Process Lynch diagnosis for a new in-age subject suitable for immediate invitation + > Run Lynch invitations > G1 (5.1) + > Process G1 letter batch > G2 (5.1) + > Run timed events > G3 (5.1) + > Book appointment > A183 (1.11) + > Process A183 letter batch > A25 (1.11) + > SC discharges patient > J24 (1.11) + > Process J24 letter batch > J25 (1.11) + > Process J25 letter batch > J26 (1.11) + > Check recall [SSCL4b(J26)] + """ + # Given I log in to BCSS "England" as user role "Hub Manager" + user_role = UserTools.user_login( + page, "Hub Manager State Registered at BCS01", return_role_type=True + ) + if user_role is None: + raise ValueError("User cannot be assigned to a UserRoleType") + + # When I receive Lynch diagnosis "EPCAM" for a new subject in my hub aged "25" with diagnosis date "1 year ago" and no last colonoscopy date + nhs_no = LynchUtils(page).insert_validated_lynch_patient_from_new_subject_with_age( + age="25", + gene="EPCAM", + when_diagnosis_took_place="1 year ago", + when_last_colonoscopy_took_place="unknown", + user_role=user_role, + ) + + # Then Comment: NHS number + assert nhs_no is not None + logging.info(f"[SUBJECT CREATION] Created Lynch subject with NHS number: {nhs_no}") + + # Then my subject has been updated as follows: + subject_assertion( + nhs_no, + { + "Calculated FOBT due date": "Null", + "Calculated lynch due date": "Null", + "Calculated surveillance due date": "Null", + "Lynch due date": "Null", + "Lynch due date date of change": "Null", + "Lynch due date reason": "Null", + "Previous screening status": "Null", + "Screening due date": "Null", + "Screening due date date of change": "Null", + "Screening due date reason": "Null", + "Subject has lynch diagnosis": "Yes", + "Subject lower FOBT age": "Default", + "Subject lower lynch age": "25", + "Screening status": "Lynch Surveillance", + "Screening status date of change": "Today", + "Screening status reason": "Eligible for Lynch Surveillance", + "Subject age": "25", + "Surveillance due date": "Null", + "Surveillance due date date of change": "Null", + "Surveillance due date reason": "Null", + }, + user_role, + ) + + # When I set the Lynch invitation rate for all screening centres to 50 + LynchUtils(page).set_lynch_invitation_rate(rate=50) + + # And I run the Lynch invitations process + GeneralRepository().run_lynch_invitations() + + # And my subject has been updated as follows: + subject_assertion( + nhs_no, + { + "Calculated FOBT due date": "Null", + "Calculated lynch due date": "Unchanged", + "Calculated surveillance due date": "Null", + "Lynch due date": "25th birthday", + "Lynch due date date of change": "Today", + "Lynch due date reason": "Selected for Lynch Surveillance", + "Previous screening status": "Null", + "Screening due date": "Null", + "Screening due date date of change": "Null", + "Screening due date reason": "Null", + "Subject has an open episode": "Yes", + "Subject has lynch diagnosis": "Yes", + "Subject lower FOBT age": "Default", + "Subject lower lynch age": "25", + "Screening status": "Lynch Surveillance", + "Screening status date of change": "Today", + "Screening status reason": "Eligible for Lynch Surveillance", + "Subject age": "25", + "Surveillance due date": "Null", + "Surveillance due date date of change": "Null", + "Surveillance due date reason": "Null", + }, + ) + + # And there is a "G1" letter batch for my subject with the exact title "Lynch Surveillance Pre-invitation" + # When I process the open "G1" letter batch for my subject + batch_processing(page, "G1", "Lynch Surveillance Pre-invitation") + + # Then my subject has been updated as follows: + subject_assertion(nhs_no, {"latest event status": "G2 Lynch Pre-invitation Sent"}) + + # When I run Timed Events for my subject + OracleDB().exec_bcss_timed_events(nhs_number=nhs_no) + + # Then my subject has been updated as follows: + subject_assertion( + nhs_no, + { + "latest event status": "G3 Lynch Surveillance Colonoscopy Assessment Appointment Required" + }, + ) + + logging.info("Progress the episode through the required pathway") + + # When I view the subject + screening_subject_page_searcher.navigate_to_subject_summary_page(page, nhs_no) + + # And I view the practitioner appointment booking screen + SubjectScreeningSummaryPage(page).click_book_practitioner_clinic_button() + + # And I select "BCS001" as the screening centre where the practitioner appointment will be held + # And I set the practitioner appointment date to "tomorrow" + # And I book the "earliest" available practitioner appointment on this date + book_appointments( + page, + "BCS001 - Wolverhampton Bowel Cancer Screening Centre", + "The Royal Hospital (Wolverhampton)", + ) + + # Then my subject has been updated as follows: + subject_assertion( + nhs_no, + { + "latest event status": "A183 1st Colonoscopy Assessment Appointment Requested", + }, + ) + + # And there is a "A183" letter batch for my subject with the exact title "Practitioner Clinic 1st Appointment (Lynch)" + # When I process the open "A183" letter batch for my subject + batch_processing(page, "A183", "Practitioner Clinic 1st Appointment (Lynch)") + + # Then my subject has been updated as follows: + subject_assertion( + nhs_no, + { + "latest event status": "A25 1st Colonoscopy Assessment Appointment Booked, letter sent", + }, + ) + + # When I switch users to BCSS "England" as user role "Screening Centre Manager" + LogoutPage(page).log_out(close_page=False) + BasePage(page).go_to_log_in_page() + UserTools.user_login(page, "Screening Centre Manager at BCS001") + + # And I view the subject + screening_subject_page_searcher.navigate_to_subject_summary_page(page, nhs_no) + + # And I view the event history for the subject's latest episode + SubjectScreeningSummaryPage(page).expand_episodes_list() + SubjectScreeningSummaryPage(page).click_first_lynch_surveillance_episode_link() + + # And I view the latest practitioner appointment in the subject's episode + EpisodeEventsAndNotesPage(page).click_most_recent_view_appointment_link() + + # And The Screening Centre cancels the practitioner appointment with reason "Patient Unsuitable - Recently Screened" + AppointmentDetailPage(page).check_cancel_radio() + AppointmentDetailPage(page).select_reason_for_cancellation_option( + "Patient Unsuitable - Recently Screened" + ) + + # And I press OK on my confirmation prompt + AppointmentDetailPage(page).click_save_button(accept_dialog=True) + + # Then my subject has been updated as follows: + subject_assertion( + nhs_no, + { + "latest event status": "J24 Screening Centre Discharge Patient", + }, + ) + + # And there is a "J24" letter batch for my subject with the exact title "Subject Discharge (Screening Centre) (Lynch)" + # When I process the open "J24" letter batch for my subject + batch_processing(page, "J24", "Subject Discharge (Screening Centre) (Lynch)") + + # Then my subject has been updated as follows: + subject_assertion( + nhs_no, + { + "latest event status": "J25 Patient discharge sent (Screening Centre discharge patient)", + }, + ) + + # When I switch users to BCSS "England" as user role "Hub Manager" + LogoutPage(page).log_out(close_page=False) + BasePage(page).go_to_log_in_page() + UserTools.user_login(page, "Hub Manager State Registered at BCS01") + + # And there is a "J25" letter batch for my subject with the exact title "GP Discharge (Discharged By Screening Centre) (Lynch)" + # When I process the open "J25" letter batch for my subject + batch_processing( + page, "J25", "GP Discharge (Discharged By Screening Centre) (Lynch)" + ) + + logging.info("Check subject details against closure scenario SSCL4b(J26)") + + # Then my subject has been updated as follows: + subject_assertion( + nhs_no, + { + "calculated fobt due date": "Null", + "calculated lynch due date": "2 years from latest J25 event", + "calculated surveillance due date": "Null", + "ceased confirmation date": "Null", + "ceased confirmation details": "Null", + "ceased confirmation user id": "Null", + "clinical reason for cease": "Null", + "latest episode accumulated result": "Lynch non-participation", + "latest episode recall calculation method": "Date of last patient letter", + "latest episode recall episode type": "Lynch Surveillance", + "latest episode recall surveillance type": "Null", + "latest episode status": "Closed", + "latest episode status reason": "Discharged", + "latest event status": "J26 GP Discharge letter sent (Discharge by Screening centre)", + "lynch due date": "Calculated Lynch due date", + "lynch due date date of change": "Today", + "lynch due date reason": "Lynch Surveillance", + "lynch incident episode": "Null", + "screening due date": "Null", + "screening due date date of change": "Unchanged", + "screening due date reason": "Unchanged", + "screening status": "Lynch Surveillance", + "screening status date of change": "Unchanged", + "screening status reason": "Lynch Surveillance", + "surveillance due date": "Null", + "surveillance due date date of change": "Unchanged", + "surveillance due date reason": "Unchanged", + }, + ) + + LogoutPage(page).log_out() diff --git a/utils/lynch_utils.py b/utils/lynch_utils.py index 1edeb5eb..31107106 100644 --- a/utils/lynch_utils.py +++ b/utils/lynch_utils.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta, date from dateutil.relativedelta import relativedelta from typing import Optional +from playwright.sync_api import Page from classes.date.date_description_utils import DateDescriptionUtils from classes.repositories.general_repository import GeneralRepository @@ -27,8 +28,12 @@ class LynchUtils: - @staticmethod + + def __init__(self, page: Page): + self.page = page + def insert_validated_lynch_patient_from_new_subject_with_age( + self, age: str, gene: str, when_diagnosis_took_place: str, @@ -64,14 +69,12 @@ def insert_validated_lynch_patient_from_new_subject_with_age( days = int(parts[2]) else: years = int(age) - days = random.randint(0, 363) + days = 0 plus_or_minus = PLUS except Exception: raise SelectionBuilderException("Invalid age format", age) - date_of_birth = datetime.today().date().replace( - day=15, month=10 - ) - relativedelta(years=years) + date_of_birth = datetime.today().date() - relativedelta(years=years) if plus_or_minus in [MINUS, LESS]: date_of_birth += timedelta(days=days) else: @@ -264,3 +267,34 @@ def process_new_lynch_patients() -> None: """ general_repository = GeneralRepository() general_repository.process_new_lynch_patients() + + def set_lynch_invitation_rate(self, rate: float) -> None: + """ + Sets the Lynch invitation rate for all screening centres. + Args: + rate (float): The new invitation rate to set. + """ + db = OracleDB() + connection = db.connect_to_db() + try: + sql = ( + "UPDATE lynch_invitation_rate lir " + "SET lir.audit_reason = 'AUTO TEST', " + "lir.datestamp = systimestamp, " + "lir.rate = :rate" + ) + with connection.cursor() as cursor: + cursor.execute(sql, {"rate": rate}) + connection.commit() + logging.info(f"Rows affected: {cursor.rowcount}") + finally: + db.disconnect_from_db(connection) + + def run_lynch_invitations(self): + """ + Executes the pkg_lynch.p_invite_lynch_subjects stored procedure to send invitations to Lynch subjects. + """ + logging.debug("Starting Lynch invitations") + general_repository = GeneralRepository() + general_repository.run_lynch_invitations() + logging.debug("Finished Lynch invitations")