Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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,23 @@ 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)
* @since 2026-03-14
*/
public int getRefusedRawValue() {
return refused - '0';
}
Comment on lines +252 to +257
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Harden raw refused decoding against unexpected DB values.

Line 252 assumes refused is always '0'..'3'. If data is malformed, this returns an out-of-contract value while JavaDoc promises 0–3.

💡 Proposed fix
     public int getRefusedRawValue() {
-        return refused - '0';
+        if (refused >= '0' && refused <= '3') {
+            return refused - '0';
+        }
+        return -1;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/io/github/carlos_emr/carlos/commn/model/Prevention.java` around
lines 251 - 253, The getRefusedRawValue method in class Prevention assumes the
char field refused is between '0' and '3'; change it to defensively validate the
refused field: if refused is between '0' and '3' return refused - '0', otherwise
return a safe default (e.g. 0) or clamp to 0–3; update method getRefusedRawValue
to perform the range check on the refused field and return the validated integer
so the method always yields 0–3 as promised.


public List<PreventionExt> getPreventionExts() {
return this.preventionExts;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,60 @@ 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 (e.g. "Smoking")
* @return boolean {@code true} if the most recent record has refused status = 0
* @since 2026-03-14
*/
public boolean isCurrentlySmoking(String preventionType) {
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,9 @@ 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).
private int refusedStatus = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Initialize refusedStatus to an unknown sentinel, not smoker status.

Line 51 defaulting to 0 means “current smoker.” If this object is created through constructors that don’t hydrate refusedStatus, it can misclassify status and reintroduce false LDCT triggers.

💡 Proposed fix
-    private int refusedStatus = 0;
+    private int refusedStatus = -1;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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).
private int refusedStatus = 0;
// 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).
private int refusedStatus = -1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/io/github/carlos_emr/carlos/prevention/PreventionItem.java`
around lines 49 - 51, The field refusedStatus in class PreventionItem is
defaulting to 0 (current smoker) which can misclassify items; change its default
to an explicit unknown sentinel (e.g., -1 or a named constant like
REFUSED_STATUS_UNKNOWN) and update the PreventionItem class to declare that
constant, initialize refusedStatus to it, and ensure all constructors and any
deserialization/hydration paths set refusedStatus explicitly (or leave as the
sentinel) so no instance is implicitly treated as 0; also update any logic that
checks refusedStatus (getters, isRefused/isSmoker methods, or switch handling)
to handle the sentinel case appropriately.

private boolean inelligible = false;
private boolean remoteEntry = false;

Expand Down Expand Up @@ -78,6 +81,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 +93,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