Skip to content

Commit b02d9a3

Browse files
committed
Merge remote-tracking branch 'origin' into feature/BCSS-21311-fobt-regression-scenario-8
# Conflicts: # pages/screening_subject_search/advance_fobt_screening_episode_page.py
2 parents dc9f2df + a42b0da commit b02d9a3

File tree

16 files changed

+1382
-32
lines changed

16 files changed

+1382
-32
lines changed

classes/date/has_user_dob_update.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def by_description(cls, description: str) -> Optional["HasUserDobUpdate"]:
3232
Optional[HasUserDobUpdate]: The matching enum member, or None if not found.
3333
"""
3434
for item in cls:
35-
if item.value == description:
35+
if item.value.lower() == description.lower():
3636
return item
3737
return None
3838

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from enum import Enum
2+
from typing import Optional
3+
4+
5+
class DeductionReasonType(Enum):
6+
"""
7+
Enum representing deduction reason types, mapped to valid value IDs, allowed values, and descriptions.
8+
Provides utility methods for lookup by description, allowed value, and valid value ID.
9+
"""
10+
11+
AFL = (307006, "AFL", "AF Enlistment (local)")
12+
AFN = (307007, "AFN", "AF Enlistment (AF)")
13+
CGA = (307008, "CGA", "Gone away")
14+
DEA = (307009, "DEA", "Death")
15+
Death = (307009, "DEA", "Death") # Alias for DEA
16+
DDR = (1, "DDR", "Deducted at Doctor's request")
17+
DIS = (307010, "DIS", "Practice dissolution")
18+
DPR = (2, "DPR", "Deducted at Patient's request")
19+
EMB = (307011, "EMB", "Embarkation")
20+
Embarkation = (307011, "EMB", "Embarkation") # Alias for EMB
21+
FP69 = (17, "FP69", "Deducted as a result of a non-respond to an FP69/")
22+
LDN = (307012, "LDN", "Logical Deletion")
23+
MH = (7, "M/H", "Mental Hospital")
24+
NIT = (307013, "NIT", "Transfer to Northern Ireland")
25+
OR = (10, "O/R", "Other")
26+
OPA = (307014, "OPA", "Address out of practice area")
27+
ORR = (307015, "ORR", "Other Reason")
28+
PAR = (13, "PAR", "Practice advise subject no longer resident")
29+
PSR = (14, "PSR", "Practice advise removal via screening system")
30+
PVR = (15, "PVR", "Practice advise removal via vaccination data")
31+
R = (26, "R", "Deducted as a result of removal to other HA")
32+
RA = (8, "R/A", "Registration/A")
33+
RC = (9, "R/C", "Registration Cancelled")
34+
RU = (27, "R/U", "Deducted as a result of return undelivered")
35+
RDI = (307016, "RDI", "Practice Request - immediate")
36+
RDR = (307017, "RDR", "Practice request")
37+
RFI = (307018, "RFI", "Residential Institute")
38+
RPR = (307019, "RPR", "Patient request")
39+
SD = (6, "S/D", "Service Dependant")
40+
SCT = (307020, "SCT", "Transferred to Scotland")
41+
SDL = (307021, "SDL", "Services Dependant (local)")
42+
SDN = (307022, "SDN", "Services Dependant (SMU)")
43+
SER = (5, "SER", "Services")
44+
TRA = (307023, "TRA", "Temporary resident not returned")
45+
46+
def __init__(
47+
self, valid_value_id: int, allowed_value: str, description: str
48+
) -> None:
49+
self._valid_value_id = valid_value_id
50+
self._allowed_value = allowed_value
51+
self._description = description
52+
53+
@property
54+
def valid_value_id(self) -> int:
55+
"""Returns the valid value ID for the deduction reason type."""
56+
return self._valid_value_id
57+
58+
@property
59+
def allowed_value(self) -> str:
60+
"""Returns the allowed value for the deduction reason type."""
61+
return self._allowed_value
62+
63+
@property
64+
def description(self) -> str:
65+
"""Returns the description for the deduction reason type."""
66+
return self._description
67+
68+
@classmethod
69+
def by_description(cls, description: str) -> Optional["DeductionReasonType"]:
70+
"""
71+
Returns the enum member matching the given description (case-sensitive).
72+
Args:
73+
description (str): The description to match.
74+
Returns:
75+
Optional[DeductionReasonType]: The matching enum member, or None if not found.
76+
"""
77+
for member in cls:
78+
if member.description == description:
79+
return member
80+
return None
81+
82+
@classmethod
83+
def by_description_case_insensitive(
84+
cls, description: str
85+
) -> Optional["DeductionReasonType"]:
86+
"""
87+
Returns the enum member matching the given description (case-insensitive).
88+
Args:
89+
description (str): The description to match.
90+
Returns:
91+
Optional[DeductionReasonType]: The matching enum member, or None if not found.
92+
"""
93+
desc_lower = description.lower()
94+
for member in cls:
95+
if member.description.lower() == desc_lower:
96+
return member
97+
return None
98+
99+
@classmethod
100+
def by_deduction_code(cls, code: str) -> Optional["DeductionReasonType"]:
101+
"""
102+
Returns the enum member matching the given allowed value (deduction code).
103+
Args:
104+
code (str): The code to match.
105+
Returns:
106+
Optional[DeductionReasonType]: The matching enum member, or None if not found.
107+
"""
108+
code_upper = code.upper()
109+
for member in cls:
110+
if member.allowed_value.upper() == code_upper:
111+
return member
112+
return None
113+
114+
@classmethod
115+
def by_valid_value_id(cls, valid_value_id: int) -> Optional["DeductionReasonType"]:
116+
"""
117+
Returns the enum member matching the given valid value ID.
118+
Args:
119+
valid_value_id (str): The valid_value_id to match.
120+
Returns:
121+
Optional[DeductionReasonType]: The matching enum member, or None if not found.
122+
"""
123+
for member in cls:
124+
if member.valid_value_id == valid_value_id:
125+
return member
126+
return None

classes/episode/subject_has_episode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def by_description(cls, description: str) -> Optional["SubjectHasEpisode"]:
3232
Optional[SubjectHasEpisode]: The matching enum member, or None if not found.
3333
"""
3434
for item in cls:
35-
if item.value == description:
35+
if item.value.lower() == description.lower():
3636
return item
3737
return None
3838

classes/repositories/subject_repository.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import logging
22
from typing import Optional
33
from classes.subject.pi_subject import PISubject
4+
from classes.subject.subject import Subject
5+
from classes.user.user import User
46
from utils.oracle.oracle import OracleDB
7+
from utils.oracle.subject_selection_query_builder import SubjectSelectionQueryBuilder
58

69

710
class SubjectRepository:
@@ -150,7 +153,7 @@ def get_active_gp_practice_in_hub_and_sc(
150153
)
151154
if df.empty:
152155
return None
153-
return df.iloc[0]["gp_code"]
156+
return df["gp_code"].iloc[0]
154157

155158
def get_inactive_gp_practice(self) -> Optional[str]:
156159
"""
@@ -168,7 +171,7 @@ def get_inactive_gp_practice(self) -> Optional[str]:
168171
df = self.oracle_db.execute_query(query)
169172
if df.empty:
170173
return None
171-
return df.iloc[0]["gp_code"]
174+
return df["gp_code"].iloc[0]
172175

173176
def get_latest_gp_practice_for_subject(self, nhs_number: str) -> Optional[str]:
174177
"""
@@ -192,4 +195,33 @@ def get_latest_gp_practice_for_subject(self, nhs_number: str) -> Optional[str]:
192195
df = self.oracle_db.execute_query(query, {"nhs_number": nhs_number})
193196
if df.empty:
194197
return None
195-
return df.iloc[0]["gp_code"]
198+
return df["gp_code"].iloc[0]
199+
200+
def get_matching_subject(
201+
self, criteria: dict, subject: Subject, user: User
202+
) -> Subject:
203+
"""
204+
Finds a subject in the DB matching the given criteria.
205+
Args:
206+
criteria (dict): The criteria you want the subject to match
207+
subject (Subject): The subject class object. Can be populated or not, depends on the criteria
208+
user (User): The user class object. Can be populated or not, depends on the criteria
209+
Returns:
210+
Subject: A populated Subject object with a subject that matches the provided criteria
211+
Raises:
212+
ValueError: If no subject matching the criteria was found.
213+
"""
214+
builder = SubjectSelectionQueryBuilder()
215+
query, bind_vars = builder.build_subject_selection_query(
216+
criteria=criteria,
217+
user=user,
218+
subject=subject,
219+
subjects_to_retrieve=1,
220+
)
221+
df = OracleDB().execute_query(query, bind_vars)
222+
try:
223+
df_row = df.iloc[0]
224+
subject = Subject().from_dataframe_row(df_row)
225+
except Exception:
226+
raise ValueError("No subject found matching the given criteria")
227+
return subject

classes/screening/has_unprocessed_sspi_updates.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def by_description(cls, description: str) -> Optional["HasUnprocessedSSPIUpdates
3232
Optional[HasUnprocessedSSPIUpdates]: The matching enum member, or None if not found.
3333
"""
3434
for item in cls:
35-
if item.value == description:
35+
if item.value.lower() == description.lower():
3636
return item
3737
return None
3838

pages/screening_practitioner_appointments/appointment_detail_page.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from playwright.sync_api import Page, expect
22
from pages.base_page import BasePage
33
from enum import StrEnum
4+
from datetime import datetime
5+
from utils.calendar_picker import CalendarPicker
46

57

68
class AppointmentDetailPage(BasePage):
@@ -87,6 +89,23 @@ def select_reason_for_cancellation_option(self, option: str) -> None:
8789
self.reason_for_cancellation_dropdown.select_option(value=option)
8890

8991

92+
def mark_appointment_as_attended(self, date: datetime) -> None:
93+
"""
94+
Marks an appointment as attended.
95+
Args:
96+
date (datetime): The date the appointment was attended
97+
"""
98+
self.wait_for_attendance_radio(
99+
600000
100+
) # Max of 10 minute wait as appointments need to be set for future times and they are in 10 minute intervals
101+
self.check_attendance_radio()
102+
self.check_attended_check_box()
103+
self.click_calendar_button()
104+
CalendarPicker(self.page).v1_calender_picker(date)
105+
self.click_save_button()
106+
self.verify_text_visible("Record updated")
107+
108+
90109
class ReasonForCancellationOptions(StrEnum):
91110
"""Enum for cancellation reason options"""
92111

pages/screening_subject_search/advance_fobt_screening_episode_page.py

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ def __init__(self, page: Page):
4545
self.record_contact_with_patient_button = self.page.get_by_role(
4646
"button", name="Record Contact with Patient"
4747
)
48+
self.amend_diagnosis_date_button = self.page.get_by_role(
49+
"button", name="Amend Diagnosis Date"
50+
)
51+
self.advance_checkbox_v2 = self.page.get_by_role("checkbox")
52+
self.subsequent_assessment_appointment_required_dropdown = (
53+
self.page.get_by_role("combobox")
54+
)
55+
self.subsequent_assessment_appointment_required_button = self.page.get_by_role(
56+
"button", name="Subsequent Assessment Appointment Required"
57+
)
4858
self.suitable_for_radiological_test_button = self.page.get_by_role(
4959
"button", name="Suitable for Radiological Test"
5060
)
@@ -58,6 +68,12 @@ def __init__(self, page: Page):
5868
"button", name="Waiting Decision to Proceed with Diagnostic Test"
5969
)
6070
)
71+
self.not_suitable_for_diagnostic_tests_button = self.page.get_by_role(
72+
"button", name="Not Suitable for Diagnostic Tests"
73+
)
74+
self.cancel_diagnostic_test_button = self.page.get_by_role(
75+
"button", name="Cancel Diagnostic Test"
76+
)
6177

6278
def click_suitable_for_endoscopic_test_button(self) -> None:
6379
"""Click the 'Suitable for Endoscopic Test' button."""
@@ -93,15 +109,21 @@ def get_latest_event_status_cell(self, latest_event_status: str) -> Locator:
93109

94110
def verify_latest_event_status_value(self, latest_event_status: str) -> None:
95111
"""Verify that the latest event status value is visible."""
96-
logging.info(f"Verifying subject has the status: {latest_event_status}")
112+
logging.info(
113+
f"[UI ASSERTION] Verifying subject has the status: {latest_event_status}"
114+
)
97115
latest_event_status_cell = self.get_latest_event_status_cell(
98116
latest_event_status
99117
)
100118
try:
101119
expect(latest_event_status_cell).to_be_visible()
102-
logging.info(f"Subject has the status: {latest_event_status}")
120+
logging.info(
121+
f"[UI ASSERTION COMPLETE] Subject has the status: {latest_event_status}"
122+
)
103123
except Exception:
104-
pytest.fail(f"Subject does not have the status: {latest_event_status}")
124+
raise AssertionError(
125+
f"[UI ASSERTION FAILED] Subject does not have the status: {latest_event_status}"
126+
)
105127

106128
def click_record_other_post_investigation_contact_button(self) -> None:
107129
"""Click the 'Record other post-investigation contact' button."""
@@ -127,6 +149,28 @@ def check_advance_checkbox(self) -> None:
127149
"""Selects the 'Advance FOBT' checkbox"""
128150
self.advance_checkbox.check()
129151

152+
def click_amend_diagnosis_date_button(self) -> None:
153+
"""Checks the 'Advance FOBT' checkbox and clicks the 'Amend Diagnosis Date' button."""
154+
self.advance_checkbox_v2.check()
155+
self.click(self.amend_diagnosis_date_button)
156+
157+
def click_and_select_subsequent_assessment_appointment_required(
158+
self, option: str
159+
) -> None:
160+
"""
161+
Click the 'Subsequent Assessment Appointment Required' button and select an option from the dropdown.
162+
Args:
163+
option (str): The option to select from the dropdown.
164+
Must be one of:
165+
- 'Previous attendance, further assessment required'
166+
- 'Interpreter requirement not identified'
167+
- 'SC interpreter DNA'
168+
"""
169+
self.subsequent_assessment_appointment_required_dropdown.select_option(
170+
label=option
171+
)
172+
self.safe_accept_dialog(self.subsequent_assessment_appointment_required_button)
173+
130174
def click_suitable_for_radiological_test_button(self) -> None:
131175
"""Click the 'Suitable for Radiological Test' button."""
132176
self.safe_accept_dialog(self.suitable_for_radiological_test_button)
@@ -148,7 +192,9 @@ def select_ct_colonography_and_invite(self) -> None:
148192
Enters today's date, selects 'CT Colonography' as the diagnostic test type,
149193
and clicks the 'Invite for Diagnostic Test' button.
150194
"""
151-
logging.info("[ADVANCE EPISODE] Selecting CT Colonography and inviting for diagnostic test")
195+
logging.info(
196+
"[ADVANCE EPISODE] Selecting CT Colonography and inviting for diagnostic test"
197+
)
152198

153199
# Step 1: Enter today's date
154200
today = date.today().strftime("%d/%m/%Y")
@@ -160,7 +206,9 @@ def select_ct_colonography_and_invite(self) -> None:
160206
logging.info("[ADVANCE EPISODE] Selected test type: CT Colonography")
161207

162208
# Step 3: Click 'Invite for Diagnostic Test'
163-
invite_button = self.page.get_by_role("button", name="Invite for Diagnostic Test >>")
209+
invite_button = self.page.get_by_role(
210+
"button", name="Invite for Diagnostic Test >>"
211+
)
164212
self.safe_accept_dialog(invite_button)
165213

166214
logging.info("[ADVANCE EPISODE] Invite for diagnostic test completed")
@@ -177,7 +225,9 @@ def record_contact_close_episode_no_contact(self) -> None:
177225
- Selects the outcome 'Close Episode - No Contact'
178226
- Clicks the save button
179227
"""
180-
logging.info("[CONTACT RECORD] Starting contact recording flow with outcome: Close Episode - No Contact")
228+
logging.info(
229+
"[CONTACT RECORD] Starting contact recording flow with outcome: Close Episode - No Contact"
230+
)
181231

182232
# Step 1: Click 'Record Contact with Patient' button
183233
self.page.get_by_role("button", name="Record Contact with Patient").click()
@@ -191,16 +241,28 @@ def record_contact_close_episode_no_contact(self) -> None:
191241
self.page.locator("#UI_START_TIME").fill("09:00")
192242
self.page.locator("#UI_END_TIME").fill("09:10")
193243
self.page.locator("#UI_DURATION").fill("10")
194-
logging.info("[CONTACT RECORD] Entered time details: 09:00–09:10, duration 10 mins")
244+
logging.info(
245+
"[CONTACT RECORD] Entered time details: 09:00–09:10, duration 10 mins"
246+
)
195247

196248
# Step 4: Enter note
197249
self.page.locator("#UI_COMMENT_ID").fill("automation test note")
198250
logging.info("[CONTACT RECORD] Entered note: automation test note")
199251

200252
# Step 5: Select outcome
201-
self.page.locator("#UI_OUTCOME").select_option(label="Close Episode - No Contact")
253+
self.page.locator("#UI_OUTCOME").select_option(
254+
label="Close Episode - No Contact"
255+
)
202256
logging.info("[CONTACT RECORD] Selected outcome: Close Episode - No Contact")
203257

204258
# Step 6: Click save
205259
self.page.locator("input[name='UI_BUTTON_SAVE']").click()
206260
logging.info("[CONTACT RECORD] Contact recording flow completed successfully")
261+
262+
def click_not_suitable_for_diagnostic_tests_button(self) -> None:
263+
"""Click the 'Not Suitable for Diagnostic Tests' button."""
264+
self.safe_accept_dialog(self.not_suitable_for_diagnostic_tests_button)
265+
266+
def click_cancel_diagnostic_test_button(self) -> None:
267+
"""Click the 'Cancel Diagnostic Test' button."""
268+
self.safe_accept_dialog(self.cancel_diagnostic_test_button)

0 commit comments

Comments
 (0)