@@ -158,6 +158,9 @@ def _add_variable_selection_criteria(
158158 self ._check_if_not_modifier_is_valid_for_criteria_key ()
159159
160160 match self .criteria_key :
161+ # ------------------------------------------------------------------------
162+ # 👤 Demographics & Subject Identity Criteria
163+ # ------------------------------------------------------------------------
161164 case SubjectSelectionCriteriaKey .NHS_NUMBER :
162165 self .criteria_value .replace (" " , "" )
163166 self ._add_criteria_nhs_number ()
@@ -168,6 +171,9 @@ def _add_variable_selection_criteria(
168171 self ._add_criteria_subject_age ()
169172 case SubjectSelectionCriteriaKey .SUBJECT_HUB_CODE :
170173 self ._add_criteria_subject_hub_code (user )
174+ # ------------------------------------------------------------------------
175+ # 🏥 Screening Centre & GP Linkage Criteria
176+ # ------------------------------------------------------------------------
171177 case (
172178 SubjectSelectionCriteriaKey .RESPONSIBLE_SCREENING_CENTRE_CODE
173179 ):
@@ -178,6 +184,9 @@ def _add_variable_selection_criteria(
178184 SubjectSelectionCriteriaKey .HAS_GP_PRACTICE_ASSOCIATED_WITH_SCREENING_CENTRE_CODE
179185 ):
180186 self ._add_criteria_has_gp_practice_linked_to_sc ()
187+ # ------------------------------------------------------------------------
188+ # 🩺 Screening Status & Change History Criteria
189+ # ------------------------------------------------------------------------
181190 case SubjectSelectionCriteriaKey .SCREENING_STATUS :
182191 self ._add_criteria_screening_status (subject )
183192 case SubjectSelectionCriteriaKey .PREVIOUS_SCREENING_STATUS :
@@ -190,6 +199,9 @@ def _add_variable_selection_criteria(
190199 self ._add_criteria_date_field (
191200 subject , "ALL_PATHWAYS" , "SCREENING_STATUS_CHANGE_DATE"
192201 )
202+ # ------------------------------------------------------------------------
203+ # ⏰ Due Dates: Screening, Surveillance & Lynch Pathways
204+ # ------------------------------------------------------------------------
193205 case SubjectSelectionCriteriaKey .PREVIOUS_LYNCH_DUE_DATE :
194206 self ._add_criteria_date_field (
195207 subject , "LYNCH" , "PREVIOUS_DUE_DATE"
@@ -246,6 +258,9 @@ def _add_variable_selection_criteria(
246258 )
247259 case SubjectSelectionCriteriaKey .BOWEL_SCOPE_DUE_DATE_REASON :
248260 self ._add_criteria_bowel_scope_due_date_reason ()
261+ # ------------------------------------------------------------------------
262+ # ⛔ Cease & Manual Override Criteria
263+ # ------------------------------------------------------------------------
249264 case SubjectSelectionCriteriaKey .MANUAL_CEASE_REQUESTED :
250265 self ._add_criteria_manual_cease_requested ()
251266 case SubjectSelectionCriteriaKey .CEASED_CONFIRMATION_DATE :
@@ -258,6 +273,9 @@ def _add_variable_selection_criteria(
258273 self ._add_criteria_ceased_confirmation_user_id (user )
259274 case SubjectSelectionCriteriaKey .CLINICAL_REASON_FOR_CEASE :
260275 self ._add_criteria_clinical_reason_for_cease ()
276+ # ------------------------------------------------------------------------
277+ # 📦 Event Status & System Update Flags
278+ # ------------------------------------------------------------------------
261279 case (
262280 SubjectSelectionCriteriaKey .SUBJECT_HAS_EVENT_STATUS
263281 | SubjectSelectionCriteriaKey .SUBJECT_DOES_NOT_HAVE_EVENT_STATUS
@@ -269,6 +287,9 @@ def _add_variable_selection_criteria(
269287 self ._add_criteria_has_unprocessed_sspi_updates ()
270288 case SubjectSelectionCriteriaKey .SUBJECT_HAS_USER_DOB_UPDATES :
271289 self ._add_criteria_has_user_dob_update ()
290+ # ------------------------------------------------------------------------
291+ # 📁 Subject Has Episode & Age-Based Criteria
292+ # ------------------------------------------------------------------------
272293 case (
273294 SubjectSelectionCriteriaKey .SUBJECT_HAS_EPISODES
274295 | SubjectSelectionCriteriaKey .SUBJECT_HAS_AN_OPEN_EPISODE
@@ -280,12 +301,16 @@ def _add_variable_selection_criteria(
280301 self ._add_criteria_subject_lower_fobt_age ()
281302 case SubjectSelectionCriteriaKey .SUBJECT_LOWER_LYNCH_AGE :
282303 self ._add_criteria_subject_lower_lynch_age ()
304+ # ------------------------------------------------------------------------
305+ # 🧱 Latest Episode Attributes
306+ # ------------------------------------------------------------------------
283307 case SubjectSelectionCriteriaKey .LATEST_EPISODE_TYPE :
284308 self ._add_criteria_latest_episode_type ()
285309 case SubjectSelectionCriteriaKey .LATEST_EPISODE_SUB_TYPE :
286310 self ._add_criteria_latest_episode_sub_type ()
287311 case SubjectSelectionCriteriaKey .LATEST_EPISODE_STATUS :
288312 self ._add_criteria_latest_episode_status ()
313+
289314 case SubjectSelectionCriteriaKey .LATEST_EPISODE_STATUS_REASON :
290315 self ._add_criteria_latest_episode_status_reason ()
291316 case (
@@ -300,6 +325,10 @@ def _add_variable_selection_criteria(
300325 SubjectSelectionCriteriaKey .LATEST_EPISODE_RECALL_SURVEILLANCE_TYPE
301326 ):
302327 self ._add_criteria_latest_episode_recall_surveillance_type ()
328+ # TODO: Continue working on the case statements below, copying the Java code
329+ # ------------------------------------------------------------------------
330+ # 🔄 Event & Workflow State Criteria
331+ # ------------------------------------------------------------------------
303332 case SubjectSelectionCriteriaKey .LATEST_EVENT_STATUS :
304333 self ._add_criteria_event_status ("ep.latest_event_status_id" )
305334 case SubjectSelectionCriteriaKey .PRE_INTERRUPT_EVENT_STATUS :
@@ -553,8 +582,6 @@ def _add_variable_selection_criteria(
553582 f"Invalid subject selection criteria key: { self .criteria_key_name } "
554583 )
555584
556- # TODO: Add more case statemented here, copying the Java code
557-
558585 except Exception :
559586 raise SelectionBuilderException (
560587 f"Invalid subject selection criteria key: { self .criteria_key_name } "
@@ -698,6 +725,203 @@ def _add_criteria_latest_episode_type(self) -> None:
698725 except Exception :
699726 raise SelectionBuilderException (self .criteria_key_name , self .criteria_value )
700727
728+ def _add_criteria_latest_episode_sub_type (self ) -> None :
729+ """
730+ Adds a SQL condition that filters based on the episode_subtype_id of a subject's latest episode.
731+
732+ Translates a human-readable episode sub-type string into an internal numeric ID.
733+ """
734+ try :
735+ value = self .criteria_value .lower ()
736+ comparator = self .criteria_comparator
737+
738+ # Simulated EpisodeSubType enum mapping
739+ episode_subtype_map = {
740+ "routine screening" : 10 ,
741+ "urgent referral" : 11 ,
742+ "pre-assessment" : 12 ,
743+ "follow-up" : 13 ,
744+ "surveillance" : 14 ,
745+ # Add more mappings as needed
746+ }
747+
748+ if value not in episode_subtype_map :
749+ raise ValueError (f"Unknown episode sub-type: { value } " )
750+
751+ episode_subtype_id = episode_subtype_map [value ]
752+
753+ # Add SQL condition using the mapped ID
754+ self .sql_where .append (
755+ f"AND ep.episode_subtype_id { comparator } { episode_subtype_id } "
756+ )
757+
758+ except Exception :
759+ raise SelectionBuilderException (self .criteria_key_name , self .criteria_value )
760+
761+ def _add_criteria_latest_episode_status (self ) -> None :
762+ """
763+ Adds a SQL condition that filters based on the episode_status_id of a subject's latest episode.
764+
765+ Translates a human-readable episode status into an internal numeric ID.
766+ """
767+ try :
768+ value = self .criteria_value .lower ()
769+ comparator = self .criteria_comparator
770+
771+ # Simulated EpisodeStatusType mapping
772+ episode_status_map = {
773+ "active" : 100 ,
774+ "completed" : 101 ,
775+ "pending" : 102 ,
776+ "cancelled" : 103 ,
777+ "invalid" : 104 ,
778+ # Add actual mappings as needed
779+ }
780+
781+ if value not in episode_status_map :
782+ raise ValueError (f"Unknown episode status: { value } " )
783+
784+ episode_status_id = episode_status_map [value ]
785+
786+ self .sql_where .append (
787+ f"AND ep.episode_status_id { comparator } { episode_status_id } "
788+ )
789+
790+ except Exception :
791+ raise SelectionBuilderException (self .criteria_key_name , self .criteria_value )
792+
793+ def _add_criteria_latest_episode_status_reason (self ) -> None :
794+ """
795+ Adds a SQL condition that filters based on the episode_status_reason_id of the subject's latest episode.
796+
797+ Allows for explicit mapping or handling of NULL where no status reason is recorded.
798+ """
799+ try :
800+ value = self .criteria_value .lower ()
801+
802+ # Simulated EpisodeStatusReasonType enum
803+ episode_status_reason_map = {
804+ "completed screening" : 200 ,
805+ "no longer eligible" : 201 ,
806+ "deceased" : 202 ,
807+ "moved away" : 203 ,
808+ "null" : None , # Special case to represent SQL IS NULL
809+ # Extend as needed
810+ }
811+
812+ if value not in episode_status_reason_map :
813+ raise ValueError (f"Unknown episode status reason: { value } " )
814+
815+ status_reason_id = episode_status_reason_map [value ]
816+
817+ if status_reason_id is None :
818+ self .sql_where .append ("AND ep.episode_status_reason_id IS NULL" )
819+ else :
820+ comparator = self .criteria_comparator
821+ self .sql_where .append (
822+ f"AND ep.episode_status_reason_id { comparator } { status_reason_id } "
823+ )
824+
825+ except Exception :
826+ raise SelectionBuilderException (self .criteria_key_name , self .criteria_value )
827+
828+ def _add_criteria_latest_episode_recall_calc_method (self ) -> None :
829+ """
830+ Adds a SQL condition filtering on recall_calculation_method_id from the latest episode.
831+
832+ Handles mapped descriptions or nulls for closed episodes with no recall method.
833+ """
834+ try :
835+ value = self .criteria_value .lower ()
836+
837+ # Simulated enum-like mapping
838+ recall_calc_method_map = {
839+ "standard" : 300 ,
840+ "accelerated" : 301 ,
841+ "paused" : 302 ,
842+ "null" : None , # For episodes with no recall method
843+ # Extend with real values as needed
844+ }
845+
846+ if value not in recall_calc_method_map :
847+ raise ValueError (f"Unknown recall calculation method: { value } " )
848+
849+ method_id = recall_calc_method_map [value ]
850+
851+ if method_id is None :
852+ self .sql_where .append ("AND ep.recall_calculation_method_id IS NULL" )
853+ else :
854+ comparator = self .criteria_comparator
855+ self .sql_where .append (
856+ f"AND ep.recall_calculation_method_id { comparator } { method_id } "
857+ )
858+
859+ except Exception :
860+ raise SelectionBuilderException (self .criteria_key_name , self .criteria_value )
861+
862+ def _add_criteria_latest_episode_recall_episode_type (self ) -> None :
863+ """
864+ Adds a filter for recall_episode_type_id based on the type of episode that triggered the recall.
865+ Supports mapped descriptions and IS NULL.
866+ """
867+ try :
868+ value = self .criteria_value .lower ()
869+
870+ recall_episode_type_map = {
871+ "referral" : 1 ,
872+ "invitation" : 2 ,
873+ "reminder" : 3 ,
874+ "episode_end" : 4 ,
875+ "null" : None ,
876+ }
877+
878+ if value not in recall_episode_type_map :
879+ raise ValueError (f"Unknown recall episode type: { value } " )
880+
881+ type_id = recall_episode_type_map [value ]
882+
883+ if type_id is None :
884+ self .sql_where .append ("AND ep.recall_episode_type_id IS NULL" )
885+ else :
886+ comparator = self .criteria_comparator
887+ self .sql_where .append (
888+ f"AND ep.recall_episode_type_id { comparator } { type_id } "
889+ )
890+
891+ except Exception :
892+ raise SelectionBuilderException (self .criteria_key_name , self .criteria_value )
893+
894+ def _add_criteria_latest_episode_recall_surveillance_type (self ) -> None :
895+ """
896+ Adds a filter for recall_polyp_surv_type_id based on the type of surveillance used during recall.
897+ Supports mapped descriptions and null values.
898+ """
899+ try :
900+ value = self .criteria_value .lower ()
901+
902+ recall_surv_type_map = {
903+ "routine" : 500 ,
904+ "enhanced" : 501 ,
905+ "annual" : 502 ,
906+ "null" : None ,
907+ }
908+
909+ if value not in recall_surv_type_map :
910+ raise ValueError (f"Unknown recall surveillance type: { value } " )
911+
912+ surv_id = recall_surv_type_map [value ]
913+
914+ if surv_id is None :
915+ self .sql_where .append ("AND ep.recall_polyp_surv_type_id IS NULL" )
916+ else :
917+ comparator = self .criteria_comparator
918+ self .sql_where .append (
919+ f"AND ep.recall_polyp_surv_type_id { comparator } { surv_id } "
920+ )
921+
922+ except Exception :
923+ raise SelectionBuilderException (self .criteria_key_name , self .criteria_value )
924+
701925 def _add_criteria_subject_hub_code (self , user : "User" ) -> None :
702926 hub_code = None
703927 try :
0 commit comments