Skip to content

Commit

Permalink
Workflow api upgrade (#2744)
Browse files Browse the repository at this point in the history
* Updated activity flow api and used that in demo app

* spotless apply

* Updated review comments

* Review comments: Added note

* review comment: fixed
  • Loading branch information
aditya-07 authored Dec 31, 2024
1 parent 69b8ebf commit a4d0929
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 129 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,36 @@
package com.google.android.fhir.workflow.activity

import androidx.annotation.WorkerThread
import ca.uhn.fhir.model.api.IQueryParameterType
import ca.uhn.fhir.rest.param.ReferenceParam
import com.google.android.fhir.workflow.activity.phase.Phase
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.ORDER
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.PERFORM
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.PLAN
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.PROPOSAL
import com.google.android.fhir.workflow.activity.phase.ReadOnlyRequestPhase
import com.google.android.fhir.workflow.activity.phase.event.PerformPhase
import com.google.android.fhir.workflow.activity.phase.event.PerformPhase.Companion.`class`
import com.google.android.fhir.workflow.activity.phase.idType
import com.google.android.fhir.workflow.activity.phase.request.OrderPhase
import com.google.android.fhir.workflow.activity.phase.request.PlanPhase
import com.google.android.fhir.workflow.activity.phase.request.ProposalPhase
import com.google.android.fhir.workflow.activity.resource.event.CPGCommunicationEvent
import com.google.android.fhir.workflow.activity.resource.event.CPGEventResource
import com.google.android.fhir.workflow.activity.resource.event.CPGOrderMedicationEvent
import com.google.android.fhir.workflow.activity.resource.event.EventStatus
import com.google.android.fhir.workflow.activity.resource.request.CPGCommunicationRequest
import com.google.android.fhir.workflow.activity.resource.request.CPGMedicationRequest
import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource
import com.google.android.fhir.workflow.activity.resource.request.Intent
import com.google.android.fhir.workflow.activity.resource.request.Status
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Communication
import org.hl7.fhir.r4.model.CommunicationRequest
import org.hl7.fhir.r4.model.MedicationDispense
import org.hl7.fhir.r4.model.MedicationRequest
import org.hl7.fhir.r4.model.Reference
import org.opencds.cqf.fhir.api.Repository

/**
Expand Down Expand Up @@ -181,6 +194,37 @@ private constructor(
return currentPhase
}

/** Returns a read only list of all the previous phases of the flow. */
fun getPreviousPhases(): List<ReadOnlyRequestPhase<R>> {
val phases = mutableListOf<ReadOnlyRequestPhase<R>>()
var current: Phase? = currentPhase

while (current != null) {
val basedOn: Reference? =
if (current is Phase.RequestPhase<*>) {
(current).getRequestResource().getBasedOn()
} else if (current is Phase.EventPhase<*>) {
(current).getEventResource().getBasedOn()
} else {
null
}

val basedOnRequest =
basedOn?.let {
repository.read(it.`class`, it.idType)?.let { CPGRequestResource.of(it) as R }
}
current =
when (basedOnRequest?.getIntent()) {
Intent.PROPOSAL -> ProposalPhase(repository, basedOnRequest)
Intent.PLAN -> PlanPhase(repository, basedOnRequest)
Intent.ORDER -> OrderPhase(repository, basedOnRequest)
else -> null
}
current?.let { phases.add(it as ReadOnlyRequestPhase<R>) }
}
return phases
}

/**
* Prepares a plan resource based on the state of the [currentPhase] and returns it to the caller
* without persisting any changes into [repository].
Expand Down Expand Up @@ -303,5 +347,116 @@ private constructor(
resource: CPGOrderMedicationEvent<*>,
): ActivityFlow<CPGMedicationRequest, CPGOrderMedicationEvent<*>> =
ActivityFlow(repository, null, resource)

/** Returns a list of active flows associated with the [patientId]. */
fun of(
repository: Repository,
patientId: String,
): List<ActivityFlow<CPGRequestResource<*>, CPGEventResource<*>>> {
/**
* NOTE: After adding a new
* [activity](https://build.fhir.org/ig/HL7/cqf-recommendations/examples-activities.html), add
* appropriate resource classes to eventTypes & requestTypes for the api to be able to search
* for flows in database.
*/
val eventTypes =
listOf(
MedicationDispense::class.java,
Communication::class.java,
)

val events =
eventTypes
.flatMap {
repository
.search(
Bundle::class.java,
it,
mutableMapOf<String, MutableList<IQueryParameterType>>(
"subject" to mutableListOf(ReferenceParam("Patient/$patientId")),
),
null,
)
.entry
.map { it.resource }
}
.map { CPGEventResource.of(it) }

val requestTypes =
listOf(
MedicationRequest::class.java,
CommunicationRequest::class.java,
)

// This is used to fetch the `basedOn` resource for a request/event to form RequestChain
val idToRequestMap: MutableMap<String, CPGRequestResource<*>> =
requestTypes
.flatMap {
repository
.search(
Bundle::class.java,
it,
mutableMapOf<String, MutableList<IQueryParameterType>>(
"subject" to mutableListOf(ReferenceParam("Patient/$patientId")),
),
null,
)
.entry
.map { it.resource }
}
.map { CPGRequestResource.of(it) }
.associateByTo(LinkedHashMap()) { "${it.resourceType}/${it.logicalId}" }

fun addBasedOn(
request: RequestChain,
): RequestChain? {
val basedOn = request.request?.getBasedOn() ?: request.event?.getBasedOn()
// look up the cache for the parent resource and add to the chain
return basedOn?.let { reference ->
idToRequestMap[reference.reference]?.let { requestResource ->
idToRequestMap.remove(reference.reference)
RequestChain(request = requestResource).apply { this.basedOn = addBasedOn(this) }
}
}
}

val requestChain =
events.map { RequestChain(event = it).apply { this.basedOn = addBasedOn(this) } } +
idToRequestMap.values
.filter {
it.getIntent() == Intent.ORDER ||
it.getIntent() == Intent.PLAN ||
it.getIntent() == Intent.PROPOSAL
}
.sortedByDescending { it.getIntent().code }
.mapNotNull {
if (idToRequestMap.containsKey("${it.resourceType}/${it.logicalId}")) {
RequestChain(request = it).apply { this.basedOn = addBasedOn(this) }
} else {
null
}
}
return requestChain
.filter {
if (it.event != null) {
it.event.getStatus() != EventStatus.COMPLETED
} else if (it.request != null) {
it.request.getStatus() != Status.COMPLETED
} else {
false
}
}
.map { ActivityFlow(repository, it.request, it.event) }
}
}
}

/**
* Represents the chain of event/requests of an activity flow. A [RequestChain] would either have a
* [request] or an [event].
*/
private data class RequestChain(
val request: CPGRequestResource<*>? = null,
val event: CPGEventResource<*>? = null,
var basedOn: RequestChain? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.google.android.fhir.workflow.activity.phase

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

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

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

Expand Down Expand Up @@ -77,3 +77,11 @@ internal fun checkEquals(a: Reference, b: Reference) = a.reference == b.referenc
/** Returns an [IdType] of a [Reference]. This is required for [Repository.read] api. */
internal val Reference.idType
get() = IdType(reference)

/** Provides a read-only view of a request phase. */
interface ReadOnlyRequestPhase<R : CPGRequestResource<*>> {
/** Returns the [Phase.PhaseName] of this phase. */
fun getPhaseName(): PhaseName

fun getRequestResource(): R
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class PerformPhase<E : CPGEventResource<*>>(
* Returns the [Resource] class for the resource. e.g. If the Reference is `Patient/1234`, then
* this would return the `Class` for `org.hl7.fhir.r4.model.Patient`.
*/
private val Reference.`class`
internal val Reference.`class`
get() = getResourceClass<Resource>(reference.split("/")[0])

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

val eventRequest = CPGEventResource.of(inputRequest, eventClass)
val eventRequest = CPGEventResource.from(inputRequest, eventClass)
eventRequest.setStatus(EventStatus.PREPARATION)
eventRequest.setBasedOn(inputRequest.asReference())
eventRequest as E
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.google.android.fhir.workflow.activity.resource.request.CPGMedicationR
import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource
import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource.Companion.of
import org.hl7.fhir.r4.model.Communication
import org.hl7.fhir.r4.model.MedicationDispense
import org.hl7.fhir.r4.model.Reference
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
Expand All @@ -41,7 +42,7 @@ import org.hl7.fhir.r4.model.ResourceType
* [CPGEventResource]s.
*/
sealed class CPGEventResource<out R>(
internal open val resource: R,
open val resource: R,
internal val mapper: EventStatusCodeMapper,
) where R : Resource {

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

companion object {

fun of(request: CPGRequestResource<*>, eventClass: Class<*>): CPGEventResource<*> {
return when (request) {
is CPGCommunicationRequest -> CPGCommunicationEvent.from(request)
is CPGMedicationRequest -> CPGOrderMedicationEvent.from(request, eventClass)
internal fun from(from: CPGRequestResource<*>, to: Class<*>): CPGEventResource<*> {
return when (from) {
is CPGCommunicationRequest -> CPGCommunicationEvent.from(from)
is CPGMedicationRequest -> CPGOrderMedicationEvent.from(from, to)
else -> {
throw IllegalArgumentException("Unknown CPG Request type ${request::class}.")
throw IllegalArgumentException("Unknown CPG Request type ${from::class}.")
}
}
}

fun of(event: Resource): CPGEventResource<*> {
return when (event) {
is Communication -> CPGCommunicationEvent(event)
is MedicationDispense -> CPGMedicationDispenseEvent(event)
else -> {
throw IllegalArgumentException("Unknown CPG event type ${event::class}.")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package com.google.android.fhir.workflow.activity.resource.request
import com.google.android.fhir.logicalId
import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource.Companion.of
import com.google.android.fhir.workflow.activity.resource.request.Intent.ORDER
import com.google.android.fhir.workflow.activity.resource.request.Intent.OTHER
import com.google.android.fhir.workflow.activity.resource.request.Intent.PLAN
import com.google.android.fhir.workflow.activity.resource.request.Intent.PROPOSAL
import org.hl7.fhir.r4.model.CommunicationRequest
Expand All @@ -28,8 +27,6 @@ import org.hl7.fhir.r4.model.MedicationRequest
import org.hl7.fhir.r4.model.Reference
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.hl7.fhir.r4.model.ServiceRequest
import org.hl7.fhir.r4.model.Task

/**
* This abstracts the
Expand All @@ -52,7 +49,7 @@ import org.hl7.fhir.r4.model.Task
* create the appropriate [CPGRequestResource].
*/
sealed class CPGRequestResource<R>(
internal open val resource: R,
open val resource: R,
internal val mapper: StatusCodeMapper,
) where R : Resource {

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

internal abstract fun setIntent(intent: Intent)

internal abstract fun getIntent(): Intent
abstract fun getIntent(): Intent

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

Expand Down Expand Up @@ -125,9 +122,7 @@ sealed class CPGRequestResource<R>(
*/
fun <R : Resource> of(resource: R): CPGRequestResource<R> {
return when (resource) {
is Task -> of(resource)
is MedicationRequest -> of(resource)
is ServiceRequest -> of(resource)
is CommunicationRequest -> of(resource)
else -> {
throw IllegalArgumentException("Unknown CPG Request type ${resource::class}.")
Expand All @@ -145,7 +140,7 @@ sealed class CPGRequestResource<R>(
* See [codesystem-request-intent](https://www.hl7.org/FHIR/codesystem-request-intent.html) for the
* list of intents.
*/
internal sealed class Intent(val code: String?) {
sealed class Intent(val code: String?) {
data object PROPOSAL : Intent("proposal")

data object PLAN : Intent("plan")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,4 +642,45 @@ class ActivityFlowTest {
// check that the flow is still in old phase (proposal).
assertThat(flow.getCurrentPhase().getPhaseName()).isEqualTo(Phase.PhaseName.PROPOSAL)
}

@Test
fun `getPreviousPhases should return a list of all previous phases`(): Unit =
runBlockingOnWorkerThread {
val cpgCommunicationRequest =
CPGRequestResource.of(
CommunicationRequest().apply {
id = "com-req-01"
status = CommunicationRequest.CommunicationRequestStatus.ACTIVE
subject = Reference("Patient/pat-01")
meta.addProfile(
"http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-communicationrequest",
)

addPayload().apply { content = StringType("Proposal") }
},
)
.apply { setIntent(Intent.PROPOSAL) }
val repository = FhirEngineRepository(FhirContext.forR4Cached(), fhirEngine)
repository.create(cpgCommunicationRequest.resource)

val flow = ActivityFlow.of(repository, cpgCommunicationRequest)

flow.initiatePlan(
flow.preparePlan().getOrThrow().apply { setStatus(Status.ACTIVE) },
)

flow.initiateOrder(
flow.prepareOrder().getOrThrow().apply { setStatus(Status.ACTIVE) },
)

flow.initiatePerform(
flow.preparePerform(CPGCommunicationEvent::class.java).getOrThrow().apply {
setStatus(EventStatus.INPROGRESS)
},
)

val result = flow.getPreviousPhases()
assertThat(result.map { it.getPhaseName() })
.containsExactly(Phase.PhaseName.ORDER, Phase.PhaseName.PLAN, Phase.PhaseName.PROPOSAL)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import com.google.android.fhir.workflow.activity.resource.request.CPGRequestReso
import com.google.android.fhir.workflow.activity.resource.request.Status

class ActivityHandler(
private val activityFlow: ActivityFlow<CPGRequestResource<*>, CPGEventResource<*>>,
val activityFlow: ActivityFlow<CPGRequestResource<*>, CPGEventResource<*>>,
) {

suspend fun prepareAndInitiatePlan(): Result<Boolean> {
Expand Down
Loading

0 comments on commit a4d0929

Please sign in to comment.