diff --git a/src/main/java/io/github/carlos_emr/carlos/commn/model/Prevention.java b/src/main/java/io/github/carlos_emr/carlos/commn/model/Prevention.java index 1e08253ad2f..dde65be821c 100644 --- a/src/main/java/io/github/carlos_emr/carlos/commn/model/Prevention.java +++ b/src/main/java/io/github/carlos_emr/carlos/commn/model/Prevention.java @@ -235,6 +235,27 @@ public String getDeletedRawValue() { return String.valueOf(deleted); } + /** + * Returns the raw integer value of the {@code refused} field. + *
+ * Standard semantics: {@code 0}=active/completed, {@code 1}=refused, + * {@code 2}=ineligible, {@code 3}=completed externally. + *
+ * 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
+ * Standard semantics: {@code 0}=active/completed, {@code 1}=refused,
+ * {@code 2}=ineligible, {@code 3}=completed externally.
+ *
+ * For the "Smoking" prevention type, the refused field is repurposed to encode
+ * smoking history status:
+ *
+ * Important note on {@code isPreventionNever("Smoking")}: 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).
+ *
+ * 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).
+ *
+ * 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;
diff --git a/src/main/java/io/github/carlos_emr/carlos/prevention/PreventionItem.java b/src/main/java/io/github/carlos_emr/carlos/prevention/PreventionItem.java
index 5b0dacdb81f..7e06f798e22 100644
--- a/src/main/java/io/github/carlos_emr/carlos/prevention/PreventionItem.java
+++ b/src/main/java/io/github/carlos_emr/carlos/prevention/PreventionItem.java
@@ -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;
@@ -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();
}
@@ -89,6 +95,23 @@ public boolean getNeverVal() {
return ret;
}
+ /**
+ * Returns the raw refused status value from the underlying prevention record.
+ *
+ * Standard semantics: {@code 0}=active/completed, {@code 1}=refused,
+ * {@code 2}=ineligible, {@code 3}=completed externally.
+ *
+ * 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.
*
diff --git a/src/main/resources/oscar/oscarPrevention/prevention.drl b/src/main/resources/oscar/oscarPrevention/prevention.drl
index cb3ac54de4a..e3e51f6c2c5 100644
--- a/src/main/resources/oscar/oscarPrevention/prevention.drl
+++ b/src/main/resources/oscar/oscarPrevention/prevention.drl
@@ -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:
@@ -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.
@@ -1162,7 +1175,7 @@ rule "LDCT 1"
eval( prev.getAgeInYears() >= 55 )
eval( prev.getAgeInYears() <= 74 )
eval( prev.getNumberOfPreventionType("Smoking") > 0 )
- eval( !prev.isPreventionNever("Smoking") )
+ eval( prev.isCurrentlySmoking("Smoking") )
eval( prev.getHowManyMonthsSinceLast("LDCT") == -1 )
eval( !prev.isPreventionNever("LDCT") )
eval( !prev.isInelligible("LDCT") )
+ *
+ *