Skip to content

Commit 8bf82f3

Browse files
authored
Fix the htnDiagnosedAt and dmDiagnosedAt for existing patients (#5666)
1 parent baf67a9 commit 8bf82f3

File tree

3 files changed

+107
-12
lines changed

3 files changed

+107
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
- Update GH Actions `setup-jdk` to to v5
3232
- Bump Gradle to v9.1.0
3333
- Bump Mockito Kotlin to v6.0.0
34+
- Add helper function `diagnosedAt` to update the htn and dm diagnosed at fields in medical history
3435

3536
### Changes
3637

app/src/main/java/org/simple/clinic/medicalhistory/MedicalHistoryRepository.kt

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.simple.clinic.medicalhistory
22

33
import io.reactivex.Completable
44
import io.reactivex.Observable
5+
import org.simple.clinic.medicalhistory.Answer.Suspected
56
import org.simple.clinic.medicalhistory.Answer.Unanswered
67
import org.simple.clinic.medicalhistory.sync.MedicalHistoryPayload
78
import org.simple.clinic.patient.PatientUuid
@@ -109,8 +110,18 @@ class MedicalHistoryRepository @Inject constructor(
109110
isSmoking = historyEntry.isSmoking,
110111
isUsingSmokelessTobacco = historyEntry.isUsingSmokelessTobacco,
111112
cholesterol = null,
112-
hypertensionDiagnosedAt = diagnosedAt(historyEntry.diagnosedWithHypertension, now),
113-
diabetesDiagnosedAt = diagnosedAt(historyEntry.hasDiabetes, now),
113+
hypertensionDiagnosedAt = diagnosedAt(
114+
existingAnswer = null,
115+
newAnswer = historyEntry.diagnosedWithHypertension,
116+
now = now,
117+
existingTimestamp = null
118+
),
119+
diabetesDiagnosedAt = diagnosedAt(
120+
existingAnswer = null,
121+
newAnswer = historyEntry.hasDiabetes,
122+
now = now,
123+
existingTimestamp = null
124+
),
114125
syncStatus = SyncStatus.PENDING,
115126
createdAt = now,
116127
updatedAt = now,
@@ -119,16 +130,20 @@ class MedicalHistoryRepository @Inject constructor(
119130
}
120131

121132
fun save(history: MedicalHistory, updateTime: Instant) {
133+
val existing = dao.getOne(history.uuid)
134+
122135
val htnDiagnosedAt = diagnosedAt(
136+
existingAnswer = existing?.diagnosedWithHypertension,
123137
newAnswer = history.diagnosedWithHypertension,
124138
now = updateTime,
125-
existingTimestamp = history.hypertensionDiagnosedAt
139+
existingTimestamp = existing?.hypertensionDiagnosedAt
126140
)
127141

128142
val diabetesDiagnosedAt = diagnosedAt(
143+
existingAnswer = existing?.diagnosedWithDiabetes,
129144
newAnswer = history.diagnosedWithDiabetes,
130145
now = updateTime,
131-
existingTimestamp = history.diabetesDiagnosedAt
146+
existingTimestamp = existing?.diabetesDiagnosedAt
132147
)
133148

134149
val dirtyHistory = history.copy(
@@ -202,14 +217,6 @@ class MedicalHistoryRepository @Inject constructor(
202217
}
203218
}
204219

205-
fun diagnosedAt(newAnswer: Answer, now: Instant, existingTimestamp: Instant? = null): Instant? {
206-
return if (!newAnswer.isAnsweredWithYesOrNo) {
207-
null
208-
} else {
209-
existingTimestamp ?: now
210-
}
211-
}
212-
213220
override fun pendingSyncRecordCount(): Observable<Int> {
214221
return dao
215222
.countWithStatus(SyncStatus.PENDING)
@@ -225,3 +232,20 @@ class MedicalHistoryRepository @Inject constructor(
225232
)
226233
}
227234
}
235+
236+
fun diagnosedAt(
237+
existingAnswer: Answer?,
238+
newAnswer: Answer,
239+
existingTimestamp: Instant?,
240+
now: Instant
241+
): Instant? {
242+
if (newAnswer == Suspected) return null
243+
244+
if (existingTimestamp != null) return existingTimestamp
245+
246+
if (existingAnswer == null || !existingAnswer.isAnsweredWithYesOrNo) {
247+
return if (newAnswer.isAnsweredWithYesOrNo) now else null
248+
}
249+
250+
return null
251+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.simple.clinic.medicalhistory
2+
3+
4+
import org.junit.Assert.assertEquals
5+
import org.junit.Assert.assertNull
6+
import org.junit.Test
7+
import org.simple.clinic.medicalhistory.Answer.No
8+
import org.simple.clinic.medicalhistory.Answer.Suspected
9+
import org.simple.clinic.medicalhistory.Answer.Unanswered
10+
import org.simple.clinic.medicalhistory.Answer.Yes
11+
import java.time.Instant
12+
13+
class DiagnosedAtTest {
14+
private val now = Instant.parse("2025-11-25T10:15:30Z")
15+
private val existingTs = Instant.parse("2025-01-01T00:00:00Z")
16+
17+
private fun call(
18+
existingAnswer: Answer?,
19+
newAnswer: Answer,
20+
existingTimestamp: Instant?,
21+
now: Instant
22+
): Instant? {
23+
return diagnosedAt(existingAnswer, newAnswer, existingTimestamp, now)
24+
}
25+
26+
@Test
27+
fun `when new answer is Suspected then returns null even if existing timestamp present`() {
28+
assertNull(call(existingAnswer = Yes, newAnswer = Suspected, existingTimestamp = existingTs, now = now))
29+
assertNull(call(existingAnswer = null, newAnswer = Suspected, existingTimestamp = existingTs, now = now))
30+
}
31+
32+
@Test
33+
fun `when existing timestamp is present and new answer is definitive then preserve the existing timestamp`() {
34+
// If timestamp exists it should be returned (write-once).
35+
assertEquals(existingTs, call(existingAnswer = Yes, newAnswer = Yes, existingTimestamp = existingTs, now = now))
36+
assertEquals(existingTs, call(existingAnswer = Suspected, newAnswer = No, existingTimestamp = existingTs, now = now))
37+
}
38+
39+
@Test
40+
fun `when existingAnswer is null and new answer is yes or no then return now`() {
41+
assertEquals(now, call(existingAnswer = null, newAnswer = Yes, existingTimestamp = null, now = now))
42+
assertEquals(now, call(existingAnswer = null, newAnswer = No, existingTimestamp = null, now = now))
43+
}
44+
45+
@Test
46+
fun `when existingAnswer is null and new answer is Suspected or Unanswered then return null`() {
47+
assertNull(call(existingAnswer = null, newAnswer = Suspected, existingTimestamp = null, now = now))
48+
assertNull(call(existingAnswer = null, newAnswer = Unanswered, existingTimestamp = null, now = now))
49+
}
50+
51+
@Test
52+
fun `when existingAnswer is suspected or unanswered and new answer is yes or no then return now`() {
53+
// Previously Suspected -> new definitive should stamp now
54+
assertEquals(now, call(existingAnswer = Suspected, newAnswer = Yes, existingTimestamp = null, now = now))
55+
assertEquals(now, call(existingAnswer = Unanswered, newAnswer = No, existingTimestamp = null, now = now))
56+
}
57+
58+
@Test
59+
fun `when existingAnswer is yes or no with no timestamp and new answer is Yes or No then return null`() {
60+
assertNull(call(existingAnswer = Yes, newAnswer = No, existingTimestamp = null, now = now))
61+
assertNull(call(existingAnswer = No, newAnswer = Yes, existingTimestamp = null, now = now))
62+
assertNull(call(existingAnswer = Yes, newAnswer = Yes, existingTimestamp = null, now = now))
63+
}
64+
65+
@Test
66+
fun `When existing timestamp is present then always preserve it except when suspected where return null`() {
67+
assertEquals(existingTs, call(existingAnswer = Yes, newAnswer = Yes, existingTimestamp = existingTs, now = now))
68+
assertNull(call(existingAnswer = Yes, newAnswer = Suspected, existingTimestamp = existingTs, now = now))
69+
}
70+
}

0 commit comments

Comments
 (0)