Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workflow api upgrade #2744

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 @@ -17,23 +17,37 @@
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.PreviousPhaseIterator
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 +195,48 @@ private constructor(
return currentPhase
}

/**
* Returns an iterator to go over the previous states of the activity flow. The iterator provides
* a read-only view to the phase.
*/
fun getPreviousPhase(): PreviousPhaseIterator<R> {
return object : PreviousPhaseIterator<R> {
var current: Phase? = currentPhase

override fun hasPrevious(): Boolean {
return if (current is Phase.RequestPhase<*>) {
(current as? Phase.RequestPhase<*>)?.getRequestResource()?.getBasedOn() != null
} else {
(current as? Phase.EventPhase<*>)?.getEventResource()?.getBasedOn() != null
}
}

override fun previous(): ReadOnlyRequestPhase<R>? {
val basedOn: Reference? =
if (current is Phase.RequestPhase<*>) {
(current as Phase.RequestPhase<*>).getRequestResource().getBasedOn()
} else if (current is Phase.EventPhase<*>) {
(current as Phase.EventPhase<*>).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
}
return basedOnRequest?.let { ReadOnlyRequestPhase(it) }
}
}
}

/**
* 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 +359,109 @@ 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<*>>> {
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,
)

val cache: 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 ->
cache[reference.reference]?.let { requestResource ->
cache.remove(reference.reference)
RequestChain(request = requestResource).apply { this.basedOn = addBasedOn(this) }
}
}
}

val requestChain =
events.map { RequestChain(event = it).apply { this.basedOn = addBasedOn(this) } } +
cache.values
.filter {
it.getIntent() == Intent.ORDER ||
it.getIntent() == Intent.PLAN ||
it.getIntent() == Intent.PROPOSAL
}
.sortedByDescending { it.getIntent().code }
.mapNotNull {
if (cache.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].
*/
internal 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 @@ -19,6 +19,7 @@ package com.google.android.fhir.workflow.activity.phase
import androidx.annotation.WorkerThread
import com.google.android.fhir.workflow.activity.resource.event.CPGEventResource
import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource
import com.google.android.fhir.workflow.activity.resource.request.Intent
import org.hl7.fhir.r4.model.IdType
import org.hl7.fhir.r4.model.Reference

Expand Down Expand Up @@ -77,3 +78,28 @@ 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)

/** Iterate through the previous phases of a current activity flow. */
interface PreviousPhaseIterator<R : CPGRequestResource<*>> {
fun hasPrevious(): Boolean

fun previous(): ReadOnlyRequestPhase<R>?
}

/** Provides a read-only view of a request phase. */
class ReadOnlyRequestPhase<R : CPGRequestResource<*>>(private val cpgRequestResource: R) {
/** Returns the [Phase.PhaseName] of this phase. */
fun getPhase() =
when (cpgRequestResource.getIntent()) {
Intent.PROPOSAL -> Phase.PhaseName.PROPOSAL
Intent.PLAN -> Phase.PhaseName.PLAN
Intent.ORDER -> Phase.PhaseName.ORDER
else ->
throw IllegalArgumentException(
"Resource intent can't be ${cpgRequestResource.getIntent().code} ",
)
}

/** Returns the underlying [CPGRequestResource] of this phase. */
fun getRequestResource() = cpgRequestResource
}
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<*> {
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 ${from::class}.")
}
}
}

fun of(request: Resource): CPGEventResource<*> {
return when (request) {
is CPGCommunicationRequest -> CPGCommunicationEvent.from(request)
is CPGMedicationRequest -> CPGOrderMedicationEvent.from(request, eventClass)
is Communication -> CPGCommunicationEvent(request)
is MedicationDispense -> CPGMedicationDispenseEvent(request)
else -> {
throw IllegalArgumentException("Unknown CPG Request type ${request::class}.")
throw IllegalArgumentException("Unknown CPG event type ${request::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 @@ -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
Loading