Skip to content

Commit a4d0929

Browse files
authored
Workflow api upgrade (#2744)
* Updated activity flow api and used that in demo app * spotless apply * Updated review comments * Review comments: Added note * review comment: fixed
1 parent 69b8ebf commit a4d0929

File tree

8 files changed

+286
-129
lines changed

8 files changed

+286
-129
lines changed

workflow/src/main/java/com/google/android/fhir/workflow/activity/ActivityFlow.kt

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,36 @@
1717
package com.google.android.fhir.workflow.activity
1818

1919
import androidx.annotation.WorkerThread
20+
import ca.uhn.fhir.model.api.IQueryParameterType
21+
import ca.uhn.fhir.rest.param.ReferenceParam
2022
import com.google.android.fhir.workflow.activity.phase.Phase
2123
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName
2224
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.ORDER
2325
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.PERFORM
2426
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.PLAN
2527
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.PROPOSAL
28+
import com.google.android.fhir.workflow.activity.phase.ReadOnlyRequestPhase
2629
import com.google.android.fhir.workflow.activity.phase.event.PerformPhase
30+
import com.google.android.fhir.workflow.activity.phase.event.PerformPhase.Companion.`class`
31+
import com.google.android.fhir.workflow.activity.phase.idType
2732
import com.google.android.fhir.workflow.activity.phase.request.OrderPhase
2833
import com.google.android.fhir.workflow.activity.phase.request.PlanPhase
2934
import com.google.android.fhir.workflow.activity.phase.request.ProposalPhase
3035
import com.google.android.fhir.workflow.activity.resource.event.CPGCommunicationEvent
3136
import com.google.android.fhir.workflow.activity.resource.event.CPGEventResource
3237
import com.google.android.fhir.workflow.activity.resource.event.CPGOrderMedicationEvent
38+
import com.google.android.fhir.workflow.activity.resource.event.EventStatus
3339
import com.google.android.fhir.workflow.activity.resource.request.CPGCommunicationRequest
3440
import com.google.android.fhir.workflow.activity.resource.request.CPGMedicationRequest
3541
import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource
3642
import com.google.android.fhir.workflow.activity.resource.request.Intent
43+
import com.google.android.fhir.workflow.activity.resource.request.Status
44+
import org.hl7.fhir.r4.model.Bundle
45+
import org.hl7.fhir.r4.model.Communication
46+
import org.hl7.fhir.r4.model.CommunicationRequest
47+
import org.hl7.fhir.r4.model.MedicationDispense
48+
import org.hl7.fhir.r4.model.MedicationRequest
49+
import org.hl7.fhir.r4.model.Reference
3750
import org.opencds.cqf.fhir.api.Repository
3851

3952
/**
@@ -181,6 +194,37 @@ private constructor(
181194
return currentPhase
182195
}
183196

197+
/** Returns a read only list of all the previous phases of the flow. */
198+
fun getPreviousPhases(): List<ReadOnlyRequestPhase<R>> {
199+
val phases = mutableListOf<ReadOnlyRequestPhase<R>>()
200+
var current: Phase? = currentPhase
201+
202+
while (current != null) {
203+
val basedOn: Reference? =
204+
if (current is Phase.RequestPhase<*>) {
205+
(current).getRequestResource().getBasedOn()
206+
} else if (current is Phase.EventPhase<*>) {
207+
(current).getEventResource().getBasedOn()
208+
} else {
209+
null
210+
}
211+
212+
val basedOnRequest =
213+
basedOn?.let {
214+
repository.read(it.`class`, it.idType)?.let { CPGRequestResource.of(it) as R }
215+
}
216+
current =
217+
when (basedOnRequest?.getIntent()) {
218+
Intent.PROPOSAL -> ProposalPhase(repository, basedOnRequest)
219+
Intent.PLAN -> PlanPhase(repository, basedOnRequest)
220+
Intent.ORDER -> OrderPhase(repository, basedOnRequest)
221+
else -> null
222+
}
223+
current?.let { phases.add(it as ReadOnlyRequestPhase<R>) }
224+
}
225+
return phases
226+
}
227+
184228
/**
185229
* Prepares a plan resource based on the state of the [currentPhase] and returns it to the caller
186230
* without persisting any changes into [repository].
@@ -303,5 +347,116 @@ private constructor(
303347
resource: CPGOrderMedicationEvent<*>,
304348
): ActivityFlow<CPGMedicationRequest, CPGOrderMedicationEvent<*>> =
305349
ActivityFlow(repository, null, resource)
350+
351+
/** Returns a list of active flows associated with the [patientId]. */
352+
fun of(
353+
repository: Repository,
354+
patientId: String,
355+
): List<ActivityFlow<CPGRequestResource<*>, CPGEventResource<*>>> {
356+
/**
357+
* NOTE: After adding a new
358+
* [activity](https://build.fhir.org/ig/HL7/cqf-recommendations/examples-activities.html), add
359+
* appropriate resource classes to eventTypes & requestTypes for the api to be able to search
360+
* for flows in database.
361+
*/
362+
val eventTypes =
363+
listOf(
364+
MedicationDispense::class.java,
365+
Communication::class.java,
366+
)
367+
368+
val events =
369+
eventTypes
370+
.flatMap {
371+
repository
372+
.search(
373+
Bundle::class.java,
374+
it,
375+
mutableMapOf<String, MutableList<IQueryParameterType>>(
376+
"subject" to mutableListOf(ReferenceParam("Patient/$patientId")),
377+
),
378+
null,
379+
)
380+
.entry
381+
.map { it.resource }
382+
}
383+
.map { CPGEventResource.of(it) }
384+
385+
val requestTypes =
386+
listOf(
387+
MedicationRequest::class.java,
388+
CommunicationRequest::class.java,
389+
)
390+
391+
// This is used to fetch the `basedOn` resource for a request/event to form RequestChain
392+
val idToRequestMap: MutableMap<String, CPGRequestResource<*>> =
393+
requestTypes
394+
.flatMap {
395+
repository
396+
.search(
397+
Bundle::class.java,
398+
it,
399+
mutableMapOf<String, MutableList<IQueryParameterType>>(
400+
"subject" to mutableListOf(ReferenceParam("Patient/$patientId")),
401+
),
402+
null,
403+
)
404+
.entry
405+
.map { it.resource }
406+
}
407+
.map { CPGRequestResource.of(it) }
408+
.associateByTo(LinkedHashMap()) { "${it.resourceType}/${it.logicalId}" }
409+
410+
fun addBasedOn(
411+
request: RequestChain,
412+
): RequestChain? {
413+
val basedOn = request.request?.getBasedOn() ?: request.event?.getBasedOn()
414+
// look up the cache for the parent resource and add to the chain
415+
return basedOn?.let { reference ->
416+
idToRequestMap[reference.reference]?.let { requestResource ->
417+
idToRequestMap.remove(reference.reference)
418+
RequestChain(request = requestResource).apply { this.basedOn = addBasedOn(this) }
419+
}
420+
}
421+
}
422+
423+
val requestChain =
424+
events.map { RequestChain(event = it).apply { this.basedOn = addBasedOn(this) } } +
425+
idToRequestMap.values
426+
.filter {
427+
it.getIntent() == Intent.ORDER ||
428+
it.getIntent() == Intent.PLAN ||
429+
it.getIntent() == Intent.PROPOSAL
430+
}
431+
.sortedByDescending { it.getIntent().code }
432+
.mapNotNull {
433+
if (idToRequestMap.containsKey("${it.resourceType}/${it.logicalId}")) {
434+
RequestChain(request = it).apply { this.basedOn = addBasedOn(this) }
435+
} else {
436+
null
437+
}
438+
}
439+
return requestChain
440+
.filter {
441+
if (it.event != null) {
442+
it.event.getStatus() != EventStatus.COMPLETED
443+
} else if (it.request != null) {
444+
it.request.getStatus() != Status.COMPLETED
445+
} else {
446+
false
447+
}
448+
}
449+
.map { ActivityFlow(repository, it.request, it.event) }
450+
}
306451
}
307452
}
453+
454+
/**
455+
* Represents the chain of event/requests of an activity flow. A [RequestChain] would either have a
456+
* [request] or an [event].
457+
*/
458+
private data class RequestChain(
459+
val request: CPGRequestResource<*>? = null,
460+
val event: CPGEventResource<*>? = null,
461+
var basedOn: RequestChain? = null,
462+
)

workflow/src/main/java/com/google/android/fhir/workflow/activity/phase/Phase.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.android.fhir.workflow.activity.phase
1818

1919
import androidx.annotation.WorkerThread
20+
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName
2021
import com.google.android.fhir.workflow.activity.resource.event.CPGEventResource
2122
import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource
2223
import org.hl7.fhir.r4.model.IdType
@@ -35,8 +36,7 @@ sealed interface Phase {
3536
fun getPhaseName(): PhaseName
3637

3738
/** Activity Phases for a CPG Request. */
38-
interface RequestPhase<R : CPGRequestResource<*>> : Phase {
39-
fun getRequestResource(): R
39+
interface RequestPhase<R : CPGRequestResource<*>> : Phase, ReadOnlyRequestPhase<R> {
4040

4141
@WorkerThread fun update(r: R): Result<Unit>
4242

@@ -77,3 +77,11 @@ internal fun checkEquals(a: Reference, b: Reference) = a.reference == b.referenc
7777
/** Returns an [IdType] of a [Reference]. This is required for [Repository.read] api. */
7878
internal val Reference.idType
7979
get() = IdType(reference)
80+
81+
/** Provides a read-only view of a request phase. */
82+
interface ReadOnlyRequestPhase<R : CPGRequestResource<*>> {
83+
/** Returns the [Phase.PhaseName] of this phase. */
84+
fun getPhaseName(): PhaseName
85+
86+
fun getRequestResource(): R
87+
}

workflow/src/main/java/com/google/android/fhir/workflow/activity/phase/event/PerformPhase.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ class PerformPhase<E : CPGEventResource<*>>(
139139
* Returns the [Resource] class for the resource. e.g. If the Reference is `Patient/1234`, then
140140
* this would return the `Class` for `org.hl7.fhir.r4.model.Patient`.
141141
*/
142-
private val Reference.`class`
142+
internal val Reference.`class`
143143
get() = getResourceClass<Resource>(reference.split("/")[0])
144144

145145
/**
@@ -165,7 +165,7 @@ class PerformPhase<E : CPGEventResource<*>>(
165165
"${inputPhase.getPhaseName().name} request is still in ${inputRequest.getStatusCode()} status."
166166
}
167167

168-
val eventRequest = CPGEventResource.of(inputRequest, eventClass)
168+
val eventRequest = CPGEventResource.from(inputRequest, eventClass)
169169
eventRequest.setStatus(EventStatus.PREPARATION)
170170
eventRequest.setBasedOn(inputRequest.asReference())
171171
eventRequest as E

workflow/src/main/java/com/google/android/fhir/workflow/activity/resource/event/CPGEventResource.kt

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.google.android.fhir.workflow.activity.resource.request.CPGMedicationR
2323
import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource
2424
import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource.Companion.of
2525
import org.hl7.fhir.r4.model.Communication
26+
import org.hl7.fhir.r4.model.MedicationDispense
2627
import org.hl7.fhir.r4.model.Reference
2728
import org.hl7.fhir.r4.model.Resource
2829
import org.hl7.fhir.r4.model.ResourceType
@@ -41,7 +42,7 @@ import org.hl7.fhir.r4.model.ResourceType
4142
* [CPGEventResource]s.
4243
*/
4344
sealed class CPGEventResource<out R>(
44-
internal open val resource: R,
45+
open val resource: R,
4546
internal val mapper: EventStatusCodeMapper,
4647
) where R : Resource {
4748

@@ -65,12 +66,22 @@ sealed class CPGEventResource<out R>(
6566

6667
companion object {
6768

68-
fun of(request: CPGRequestResource<*>, eventClass: Class<*>): CPGEventResource<*> {
69-
return when (request) {
70-
is CPGCommunicationRequest -> CPGCommunicationEvent.from(request)
71-
is CPGMedicationRequest -> CPGOrderMedicationEvent.from(request, eventClass)
69+
internal fun from(from: CPGRequestResource<*>, to: Class<*>): CPGEventResource<*> {
70+
return when (from) {
71+
is CPGCommunicationRequest -> CPGCommunicationEvent.from(from)
72+
is CPGMedicationRequest -> CPGOrderMedicationEvent.from(from, to)
7273
else -> {
73-
throw IllegalArgumentException("Unknown CPG Request type ${request::class}.")
74+
throw IllegalArgumentException("Unknown CPG Request type ${from::class}.")
75+
}
76+
}
77+
}
78+
79+
fun of(event: Resource): CPGEventResource<*> {
80+
return when (event) {
81+
is Communication -> CPGCommunicationEvent(event)
82+
is MedicationDispense -> CPGMedicationDispenseEvent(event)
83+
else -> {
84+
throw IllegalArgumentException("Unknown CPG event type ${event::class}.")
7485
}
7586
}
7687
}

workflow/src/main/java/com/google/android/fhir/workflow/activity/resource/request/CPGRequestResource.kt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package com.google.android.fhir.workflow.activity.resource.request
1919
import com.google.android.fhir.logicalId
2020
import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource.Companion.of
2121
import com.google.android.fhir.workflow.activity.resource.request.Intent.ORDER
22-
import com.google.android.fhir.workflow.activity.resource.request.Intent.OTHER
2322
import com.google.android.fhir.workflow.activity.resource.request.Intent.PLAN
2423
import com.google.android.fhir.workflow.activity.resource.request.Intent.PROPOSAL
2524
import org.hl7.fhir.r4.model.CommunicationRequest
@@ -28,8 +27,6 @@ import org.hl7.fhir.r4.model.MedicationRequest
2827
import org.hl7.fhir.r4.model.Reference
2928
import org.hl7.fhir.r4.model.Resource
3029
import org.hl7.fhir.r4.model.ResourceType
31-
import org.hl7.fhir.r4.model.ServiceRequest
32-
import org.hl7.fhir.r4.model.Task
3330

3431
/**
3532
* This abstracts the
@@ -52,7 +49,7 @@ import org.hl7.fhir.r4.model.Task
5249
* create the appropriate [CPGRequestResource].
5350
*/
5451
sealed class CPGRequestResource<R>(
55-
internal open val resource: R,
52+
open val resource: R,
5653
internal val mapper: StatusCodeMapper,
5754
) where R : Resource {
5855

@@ -64,7 +61,7 @@ sealed class CPGRequestResource<R>(
6461

6562
internal abstract fun setIntent(intent: Intent)
6663

67-
internal abstract fun getIntent(): Intent
64+
abstract fun getIntent(): Intent
6865

6966
abstract fun setStatus(status: Status, reason: String? = null)
7067

@@ -125,9 +122,7 @@ sealed class CPGRequestResource<R>(
125122
*/
126123
fun <R : Resource> of(resource: R): CPGRequestResource<R> {
127124
return when (resource) {
128-
is Task -> of(resource)
129125
is MedicationRequest -> of(resource)
130-
is ServiceRequest -> of(resource)
131126
is CommunicationRequest -> of(resource)
132127
else -> {
133128
throw IllegalArgumentException("Unknown CPG Request type ${resource::class}.")
@@ -145,7 +140,7 @@ sealed class CPGRequestResource<R>(
145140
* See [codesystem-request-intent](https://www.hl7.org/FHIR/codesystem-request-intent.html) for the
146141
* list of intents.
147142
*/
148-
internal sealed class Intent(val code: String?) {
143+
sealed class Intent(val code: String?) {
149144
data object PROPOSAL : Intent("proposal")
150145

151146
data object PLAN : Intent("plan")

workflow/src/test/java/com/google/android/fhir/workflow/activity/ActivityFlowTest.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,4 +642,45 @@ class ActivityFlowTest {
642642
// check that the flow is still in old phase (proposal).
643643
assertThat(flow.getCurrentPhase().getPhaseName()).isEqualTo(Phase.PhaseName.PROPOSAL)
644644
}
645+
646+
@Test
647+
fun `getPreviousPhases should return a list of all previous phases`(): Unit =
648+
runBlockingOnWorkerThread {
649+
val cpgCommunicationRequest =
650+
CPGRequestResource.of(
651+
CommunicationRequest().apply {
652+
id = "com-req-01"
653+
status = CommunicationRequest.CommunicationRequestStatus.ACTIVE
654+
subject = Reference("Patient/pat-01")
655+
meta.addProfile(
656+
"http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-communicationrequest",
657+
)
658+
659+
addPayload().apply { content = StringType("Proposal") }
660+
},
661+
)
662+
.apply { setIntent(Intent.PROPOSAL) }
663+
val repository = FhirEngineRepository(FhirContext.forR4Cached(), fhirEngine)
664+
repository.create(cpgCommunicationRequest.resource)
665+
666+
val flow = ActivityFlow.of(repository, cpgCommunicationRequest)
667+
668+
flow.initiatePlan(
669+
flow.preparePlan().getOrThrow().apply { setStatus(Status.ACTIVE) },
670+
)
671+
672+
flow.initiateOrder(
673+
flow.prepareOrder().getOrThrow().apply { setStatus(Status.ACTIVE) },
674+
)
675+
676+
flow.initiatePerform(
677+
flow.preparePerform(CPGCommunicationEvent::class.java).getOrThrow().apply {
678+
setStatus(EventStatus.INPROGRESS)
679+
},
680+
)
681+
682+
val result = flow.getPreviousPhases()
683+
assertThat(result.map { it.getPhaseName() })
684+
.containsExactly(Phase.PhaseName.ORDER, Phase.PhaseName.PLAN, Phase.PhaseName.PROPOSAL)
685+
}
645686
}

workflow_demo/src/main/java/com/google/android/fhir/workflow/demo/ui/main/ActivityHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import com.google.android.fhir.workflow.activity.resource.request.CPGRequestReso
2323
import com.google.android.fhir.workflow.activity.resource.request.Status
2424

2525
class ActivityHandler(
26-
private val activityFlow: ActivityFlow<CPGRequestResource<*>, CPGEventResource<*>>,
26+
val activityFlow: ActivityFlow<CPGRequestResource<*>, CPGEventResource<*>>,
2727
) {
2828

2929
suspend fun prepareAndInitiatePlan(): Result<Boolean> {

0 commit comments

Comments
 (0)