Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,27 @@ public String getDeletedRawValue() {
return String.valueOf(deleted);
}

/**
* Returns the raw integer value of the {@code refused} field.
* <p>
* Standard semantics: {@code 0}=active/completed, {@code 1}=refused,
* {@code 2}=ineligible, {@code 3}=completed externally.
* <p>
* For the "Smoking" prevention type, this field is repurposed to encode
* smoking history status: {@code 0}=Yes (current smoker), {@code 1}=No (non-smoker),
* {@code 2}=Previous (ex-smoker).
*
* @return int raw refused status value (0, 1, 2, or 3), or {@code -1} if the stored
* value is outside the expected range
* @since 2026-03-14
*/
public int getRefusedRawValue() {
if (refused >= '0' && refused <= '3') {
return refused - '0';
}
return -1;
}

public List<PreventionExt> getPreventionExts() {
return this.preventionExts;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ public boolean isPreventionNever(String preventionType) {
}

public boolean isNotPreventionNever(String preventionType) {
return !isNotPreventionNever(preventionType);
return !isPreventionNever(preventionType);
}


Expand All @@ -302,6 +302,65 @@ public boolean isNotInelligible(String preventionType) {
return !isInelligible(preventionType);
}

/**
* Returns the raw refused status value for the most recent prevention record of the given type.
* <p>
* Standard semantics: {@code 0}=active/completed, {@code 1}=refused,
* {@code 2}=ineligible, {@code 3}=completed externally.
* <p>
* For the "Smoking" prevention type, the refused field is repurposed to encode
* smoking history status:
* <ul>
* <li>{@code 0} - Currently smoking ("Yes")</li>
* <li>{@code 1} - Not smoking ("No")</li>
* <li>{@code 2} - Previously smoked ("Previous")</li>
* </ul>
* <p>
* <b>Important note on {@code isPreventionNever("Smoking")}:</b> The {@code never}
* column in the preventions table is separate from the {@code refused} column and is
* always {@code 0} for all three smoking states. Therefore {@code isPreventionNever}
* cannot be used to detect non-smokers; use {@code isCurrentlySmoking} instead.
*
* @param preventionType String the prevention type key (e.g. "Smoking", "LDCT")
* @return int the raw refused status value, or {@code -1} if no record exists
* @since 2026-03-14
*/
public int getRefusedStatus(String preventionType) {
int status = -1;
Vector vec = (Vector) preventionTypes.get(preventionType);
if (vec != null && !vec.isEmpty()) {
PreventionItem p = (PreventionItem) vec.get(vec.size() - 1);
status = p.getRefusedStatus();
}
return status;
}

/**
* Returns {@code true} if the most recent prevention record for the given type indicates
* a "currently active" or "yes" response (refused status = 0).
* <p>
* For the "Smoking" prevention type specifically, this returns {@code true} when the
* patient is a current smoker. The Smoking assessment stores its answer in the
* {@code refused} field: {@code 0}=Yes (current smoker), {@code 1}=No (non-smoker),
* {@code 2}=Previous (ex-smoker).
* <p>
* Use this method instead of {@code !isPreventionNever("Smoking")} for LDCT eligibility
* checks. The {@code never} column is always {@code 0} for all smoking states and cannot
* distinguish current smokers from non-smokers.
*
* @param preventionType String the prevention type key; must be {@code "Smoking"} —
* returns {@code false} for any other type to prevent semantic misuse
* @return boolean {@code true} if preventionType is "Smoking" and the most recent record
* has refused status = 0 (current smoker)
* @since 2026-03-14
*/
public boolean isCurrentlySmoking(String preventionType) {
if (!"Smoking".equals(preventionType)) {
return false;
}
return getRefusedStatus(preventionType) == 0;
}


public int getHowManyMonthsSinceLast(String preventionType) {
int retval = -1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ public class PreventionItem {
Date nextDate = null;
String never = null;
boolean refused;
// Raw refused status value (0=active, 1=refused, 2=ineligible, 3=completedExternally).
// For the "Smoking" type: 0=Yes (current), 1=No (non-smoker), 2=Previous (ex-smoker).
// Defaults to -1 (unknown/uninitialized) to avoid misclassifying items built via
// constructors that do not hydrate this field from a Prevention record.
private int refusedStatus = -1;
private boolean inelligible = false;
private boolean remoteEntry = false;

Expand Down Expand Up @@ -78,6 +83,7 @@ public PreventionItem(Prevention pp) {
this.never = ConversionUtils.toBoolString(pp.isNever());
this.nextDate = pp.getNextDate();
this.refused = pp.isRefused();
this.refusedStatus = pp.getRefusedRawValue();
this.inelligible = pp.isIneligible();
}

Expand All @@ -89,6 +95,23 @@ public boolean getNeverVal() {
return ret;
}

/**
* Returns the raw refused status value from the underlying prevention record.
* <p>
* Standard semantics: {@code 0}=active/completed, {@code 1}=refused,
* {@code 2}=ineligible, {@code 3}=completed externally.
* <p>
* For the "Smoking" prevention type, this field is repurposed to encode
* smoking history: {@code 0}=Yes (current smoker), {@code 1}=No (non-smoker),
* {@code 2}=Previous (ex-smoker).
*
* @return int raw refused status value
* @since 2026-03-14
*/
public int getRefusedStatus() {
return refusedStatus;
}

/**
* Getter for property datePreformed.
*
Expand Down
25 changes: 19 additions & 6 deletions src/main/resources/oscar/oscarPrevention/prevention.drl
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
// Status: isPreventionNever(String), isNextDateSet(String), isPassedNextDate(String),
// isInelligible(String), isLastPreventionWithinRange(String, String, String),
// isTodayinDateRange(String, String)
// Smoking: isCurrentlySmoking(String), getRefusedStatus(String)
// NOTE: isPreventionNever("Smoking") is INCORRECT for smoking checks — the
// "never" column is always 0 for all smoking states (Yes/No/Previous).
// The smoking answer is stored in the "refused" column:
// 0=Yes (current smoker), 1=No (non-smoker), 2=Previous (ex-smoker).
// Use isCurrentlySmoking("Smoking") to check for active smoking status.
// Sex: isMale(), isFemale()
//
// Consequence methods:
Expand Down Expand Up @@ -1144,13 +1150,20 @@ end
// LDCT: Low-Dose CT Lung Cancer Screening (2 rules)
// Canadian guideline (CTFPHC 2016): Annual LDCT for adults aged 55-74 who are
// high-risk smokers (30+ pack-years or equivalent significant smoking history)
// Rule 1: WARNING if no LDCT on record for patients aged 55-74 with positive smoking history
// Rule 1: WARNING if no LDCT on record for patients aged 55-74 who are current smokers
// Rule 2: WARNING if LDCT count is 1 or 2 and it is > 12 months old (annual x3 years)
// LDCT 2 stops firing after 3rd annual screen (count >= 3).
// Precondition (heavy smoker detection): Smoking assessment must be on record AND
// not marked as "never" (non-smoker). This uses the Prevention module's "Smoking" type.
// A Smoking record exists (getNumberOfPreventionType("Smoking") > 0) AND
// the record is NOT marked as never-smoked (!isPreventionNever("Smoking")).
//
// Smoking status check (LDCT 1):
// The Smoking prevention type uses the "refused" column (NOT the "never" column) to
// store the patient's smoking answer. The "never" column is always 0 for all smoking
// states, so isPreventionNever("Smoking") cannot be used here.
// Smoking refused column encoding:
// 0 = Yes (currently smoking) <-- LDCT rule should fire
// 1 = No (not smoking) <-- LDCT rule should NOT fire
// 2 = Previous (ex-smoker) <-- LDCT rule should NOT fire
// isCurrentlySmoking("Smoking") returns true only when refused = 0.
//
// Note: Full heavy-smoker detection (pack-years, cigarettes/day) requires measurement
// values (NOSK > 23, POSK > 0, SKST = yes, SMK = yes, SmkS > 23) which are stored in
// the measurements table and are not accessible from the Prevention DRL fact object.
Expand All @@ -1162,7 +1175,7 @@ rule "LDCT 1"
eval( prev.getAgeInYears() >= 55 )
eval( prev.getAgeInYears() <= 74 )
eval( prev.getNumberOfPreventionType("Smoking") > 0 )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Removing the !prev.isPreventionNever("Smoking") check makes this rule trigger for patients who are documented non-smokers. The rule is intended for high-risk smokers, so this change will generate incorrect clinical warnings.

The comment block for the LDCT rules (lines 1150-1153) explicitly states that the logic should check that the patient is not a "never-smoker".

// Precondition (heavy smoker detection): Smoking assessment must be on record AND
//   not marked as "never" (non-smoker). This uses the Prevention module's "Smoking" type.
//   A Smoking record exists (getNumberOfPreventionType("Smoking") > 0) AND
//   the record is NOT marked as never-smoked (!isPreventionNever("Smoking")).

This change makes the implementation inconsistent with the documentation and the clinical purpose of the rule. The check should be restored.

If this change is intentional, the comment block and the rule's warning message should be updated to reflect that it no longer targets only smokers.

        eval( prev.getNumberOfPreventionType("Smoking") > 0 )
        eval( !prev.isPreventionNever("Smoking") )

eval( !prev.isPreventionNever("Smoking") )
eval( prev.isCurrentlySmoking("Smoking") )
eval( prev.getHowManyMonthsSinceLast("LDCT") == -1 )
eval( !prev.isPreventionNever("LDCT") )
eval( !prev.isInelligible("LDCT") )
Expand Down
Loading