@@ -390,6 +390,9 @@ def _add_variable_selection_criteria(
390390 SubjectSelectionCriteriaKey .HAS_DIAGNOSTIC_TEST_CONTAINING_POLYP
391391 ):
392392 self ._add_criteria_has_diagnostic_test_containing_polyp ()
393+ # ------------------------------------------------------------------------
394+ # 📦 Kit Metadata & Participation History
395+ # ------------------------------------------------------------------------
393396 case SubjectSelectionCriteriaKey .SUBJECT_HAS_UNLOGGED_KITS :
394397 self ._add_criteria_subject_has_unlogged_kits ()
395398 case SubjectSelectionCriteriaKey .SUBJECT_HAS_LOGGED_FIT_KITS :
@@ -1261,6 +1264,104 @@ def _add_criteria_has_diagnostic_test_containing_polyp(self) -> None:
12611264 except Exception :
12621265 raise SelectionBuilderException (self .criteria_key_name , self .criteria_value )
12631266
1267+ def _add_criteria_subject_has_unlogged_kits (self ) -> None :
1268+ """
1269+ Adds a filter to check for unlogged kits across subject history,
1270+ or scoped to the latest episode. Accepts:
1271+ - "yes"
1272+ - "yes_latest_episode"
1273+ - "no"
1274+ """
1275+ try :
1276+ value = self .criteria_value .strip ().lower ()
1277+
1278+ if value in ("yes" , "yes_latest_episode" ):
1279+ prefix = "AND EXISTS"
1280+ elif value == "no" :
1281+ prefix = "AND NOT EXISTS"
1282+ else :
1283+ raise ValueError (f"Unknown value for unlogged kits: { value } " )
1284+
1285+ subquery = [
1286+ f"{ prefix } (" ,
1287+ " SELECT 'tku'" ,
1288+ " FROM tk_items_t tku" ,
1289+ " WHERE tku.screening_subject_id = ss.screening_subject_id" ,
1290+ ]
1291+
1292+ if value == "yes_latest_episode" :
1293+ self ._add_join_to_latest_episode ()
1294+ subquery .append (" AND tku.subject_epis_id = ep.subject_epis_id" )
1295+
1296+ subquery .append (" AND tku.logged_in_flag = 'N'" )
1297+ subquery .append (")" )
1298+
1299+ self .sql_where .append ("\n " .join (subquery ))
1300+
1301+ except Exception :
1302+ raise SelectionBuilderException (self .criteria_key_name , self .criteria_value )
1303+
1304+ def _add_criteria_subject_has_logged_fit_kits (self ) -> None :
1305+ """
1306+ Adds a filter to check if the subject has logged FIT kits (tk_type_id > 1 and logged_in_flag = 'Y').
1307+ Accepts values: 'yes' or 'no'.
1308+ """
1309+ try :
1310+ value = self .criteria_value .strip ().lower ()
1311+
1312+ if value == "yes" :
1313+ prefix = "AND EXISTS"
1314+ elif value == "no" :
1315+ prefix = "AND NOT EXISTS"
1316+ else :
1317+ raise ValueError (f"Invalid value for logged FIT kits: { value } " )
1318+
1319+ self .sql_where .append (
1320+ f"""{ prefix } (
1321+ SELECT 'tkl'
1322+ FROM tk_items_t tkl
1323+ WHERE tkl.screening_subject_id = ss.screening_subject_id
1324+ AND tkl.tk_type_id > 1
1325+ AND tkl.logged_in_flag = 'Y'
1326+ )"""
1327+ )
1328+
1329+ except Exception :
1330+ raise SelectionBuilderException (self .criteria_key_name , self .criteria_value )
1331+
1332+ def _add_criteria_subject_has_kit_notes (self ) -> None :
1333+ """
1334+ Filters subjects based on presence of active kit-related notes.
1335+ Accepts values: 'yes' or 'no'.
1336+ """
1337+ try :
1338+ value = self .criteria_value .strip ().lower ()
1339+
1340+ if value == "yes" :
1341+ prefix = "AND EXISTS"
1342+ elif value == "no" :
1343+ prefix = "AND NOT EXISTS"
1344+ else :
1345+ raise ValueError (f"Invalid value for kit notes: { value } " )
1346+
1347+ self .sql_where .append (
1348+ f"""{ prefix } (
1349+ SELECT 1
1350+ FROM supporting_notes_t sn
1351+ WHERE sn.screening_subject_id = ss.screening_subject_id
1352+ AND (
1353+ sn.type_id = '308015'
1354+ OR sn.promote_pio_id IS NOT NULL
1355+ )
1356+ AND sn.status_id = 4100
1357+ )
1358+ AND ss.number_of_invitations > 0
1359+ AND rownum = 1"""
1360+ )
1361+
1362+ except Exception :
1363+ raise SelectionBuilderException (self .criteria_key_name , self .criteria_value )
1364+
12641365 def _add_criteria_subject_hub_code (self , user : "User" ) -> None :
12651366 hub_code = None
12661367 try :
0 commit comments