Skip to content

Commit 8924bff

Browse files
committed
Add separate endpoint for create and invite participant group methods to RecruitmentService
Also marks `InviteNewParticipantGroup` Endpoint and related method as deprecated, and replace their usage with new create and invite method respectively.
1 parent 50ef147 commit 8924bff

File tree

33 files changed

+2854
-61
lines changed

33 files changed

+2854
-61
lines changed

carp.studies.core/src/commonMain/kotlin/dk/cachet/carp/studies/application/RecruitmentService.kt

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,49 @@ interface RecruitmentService : ApplicationService<RecruitmentService, Recruitmen
7474
* - not all necessary participant roles part of the study have been assigned a participant
7575
* @throws IllegalStateException when the study is not yet ready for deployment.
7676
*/
77+
@Deprecated(
78+
"Use createParticipantGroup and inviteParticipantGroup instead",
79+
ReplaceWith(
80+
"inviteParticipantGroup( createParticipantGroup( UUID.randomUUID(), group, studyId, name ).id )",
81+
"dk.cachet.carp.common.application.UUID"
82+
),
83+
level = DeprecationLevel.WARNING
84+
)
7785
suspend fun inviteNewParticipantGroup(
7886
studyId: UUID,
7987
group: Set<AssignedParticipantRoles>,
8088
name: String? = null
8189
): ParticipantGroupStatus
8290

8391
/**
84-
* Get the status of all deployed participant groups in the study with the specified [studyId].
92+
* Create a new participant [group] of previously added participants for the study with the given [studyId],
93+
* and an optional [name] representing this group, but do not yet send out invitations.
94+
* This is used to create a group of participants which can be deployed at a later time.
95+
* @throws IllegalArgumentException when:
96+
* - a study with [studyId] does not exist
97+
* - an existing participant group with [groupId] already exists
98+
* - any of the participant roles specified in [group] does not exist
99+
* @throws IllegalStateException when the study is not yet ready for deployment.
100+
*/
101+
suspend fun createParticipantGroup(
102+
groupId: UUID,
103+
group: Set<AssignedParticipantRoles>,
104+
studyId: UUID,
105+
name: String? = null
106+
): ParticipantGroupStatus
107+
108+
/**
109+
* Invite the participant group with the specified [groupId] to start participating in its study.
110+
*
111+
* @throws IllegalArgumentException when:
112+
* - the participant group with [groupId] does not exist.
113+
* - not all necessary participant roles part of the study have been assigned a participant
114+
* @throws IllegalStateException when group has already been deployed.
115+
*/
116+
suspend fun inviteParticipantGroup( groupId: UUID ): ParticipantGroupStatus
117+
118+
/**
119+
* Get the status of all participant groups in the study with the specified [studyId].
85120
*
86121
* @throws IllegalArgumentException when a study with [studyId] does not exist.
87122
*/

carp.studies.core/src/commonMain/kotlin/dk/cachet/carp/studies/application/RecruitmentServiceHost.kt

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import dk.cachet.carp.studies.application.users.Participant
1616
import dk.cachet.carp.studies.application.users.ParticipantGroupStatus
1717
import dk.cachet.carp.studies.domain.users.ParticipantRepository
1818
import dk.cachet.carp.studies.domain.users.Recruitment
19+
import dk.cachet.carp.studies.domain.users.StagedParticipantGroup
1920
import kotlinx.datetime.Clock
2021

2122

@@ -124,6 +125,13 @@ class RecruitmentServiceHost(
124125
* - not all necessary participant roles part of the study have been assigned a participant
125126
* @throws IllegalStateException when the study is not yet ready for deployment.
126127
*/
128+
@Deprecated(
129+
"Use createParticipantGroup and inviteParticipantGroup instead",
130+
replaceWith = ReplaceWith(
131+
"inviteParticipantGroup( createParticipantGroup( UUID.randomUUID(), group, studyId, name ).id )",
132+
"dk.cachet.carp.common.application.UUID"
133+
)
134+
)
127135
override suspend fun inviteNewParticipantGroup(
128136
studyId: UUID,
129137
group: Set<AssignedParticipantRoles>,
@@ -160,21 +168,86 @@ class RecruitmentServiceHost(
160168
}
161169

162170
/**
163-
* Get the status of all deployed participant groups in the study with the specified [studyId].
171+
* Create a new participant [group] of previously added participants for the study with the given [studyId],
172+
* and an optional [name] representing this group, but do not yet send out invitations.
173+
* This is used to create a group of participants which can be deployed at a later time.
174+
* @throws IllegalArgumentException when:
175+
* - a study with [studyId] does not exist
176+
* - an existing participant group with [groupId] already exists
177+
* - any of the participant roles specified in [group] does not exist
178+
* @throws IllegalStateException when the study is not yet ready for deployment.
179+
*/
180+
override suspend fun createParticipantGroup(
181+
groupId: UUID,
182+
group: Set<AssignedParticipantRoles>,
183+
studyId: UUID,
184+
name: String?
185+
): ParticipantGroupStatus
186+
{
187+
val recruitment = getRecruitmentOrThrow( studyId )
188+
189+
require( recruitment.participantGroups[ groupId ] == null ) {
190+
"Participant group with ID \"$groupId\" already exists."
191+
}
192+
193+
val participantGroup = recruitment.addParticipantGroup( group, name, groupId )
194+
participantRepository.updateRecruitment( recruitment )
195+
196+
return recruitment.getParticipantGroupStatus( participantGroup )
197+
}
198+
199+
/**
200+
* Invite the participant group with the specified [groupId] to start participating in its study.
201+
*
202+
* @throws IllegalArgumentException when:
203+
* - the participant group with [groupId] does not exist.
204+
* - not all necessary participant roles part of the study have been assigned a participant
205+
* @throws IllegalStateException when group has already been deployed.
206+
*/
207+
override suspend fun inviteParticipantGroup( groupId: UUID ): ParticipantGroupStatus
208+
{
209+
val (recruitment, participantGroup) = getRecruitmentAndGroupByGroupIdOrThrow( groupId )
210+
check( !participantGroup.isDeployed ) { "Participant group has already been deployed." }
211+
212+
val (protocol, invitations) = recruitment.createInvitations( participantGroup.roleAssignments )
213+
val deploymentStatus = deploymentService.createStudyDeployment( participantGroup.id, protocol, invitations )
214+
215+
// Mark participant group as deployed.
216+
// TODO: If `createStudyDeployment` succeed, but the 'updateRecruitment' fails, state will be inconsistent.
217+
// This should use eventual consistency: https://github.com/imotions/carp.core-kotlin/issues/295
218+
participantGroup.markAsDeployed()
219+
participantRepository.updateRecruitment( recruitment )
220+
221+
return recruitment.getParticipantGroupStatus( deploymentStatus )
222+
}
223+
224+
/**
225+
* Get the status of all participant groups in the study with the specified [studyId].
164226
*
165227
* @throws IllegalArgumentException when a study with [studyId] does not exist.
166228
*/
167229
override suspend fun getParticipantGroupStatusList( studyId: UUID ): List<ParticipantGroupStatus>
168230
{
169231
val recruitment: Recruitment = getRecruitmentOrThrow( studyId )
170232

171-
// Get study deployment status list.
172-
val studyDeploymentIds = recruitment.participantGroups.keys
173-
val studyDeploymentStatusList: List<StudyDeploymentStatus> =
174-
if ( studyDeploymentIds.isEmpty() ) emptyList()
175-
else deploymentService.getStudyDeploymentStatusList( studyDeploymentIds )
233+
// Get status list for staged participant groups.
234+
val stagedParticipantGroups = recruitment.participantGroups
235+
.filter { !it.value.isDeployed }
236+
.values
237+
.map { recruitment.getParticipantGroupStatus( it ) }
238+
239+
// Get status list for deployed participant groups.
240+
val deployedStudyDeploymentIds = recruitment.participantGroups
241+
.filter { it.value.isDeployed }
242+
.keys
176243

177-
return studyDeploymentStatusList.map { recruitment.getParticipantGroupStatus( it ) }
244+
val studyDeploymentStatusList =
245+
if ( deployedStudyDeploymentIds.isEmpty() ) emptyList()
246+
else deploymentService
247+
.getStudyDeploymentStatusList( deployedStudyDeploymentIds )
248+
.map { recruitment.getParticipantGroupStatus( it ) }
249+
250+
return stagedParticipantGroups + studyDeploymentStatusList
178251
}
179252

180253
/**
@@ -204,4 +277,17 @@ class RecruitmentServiceHost(
204277
private suspend fun getRecruitmentOrThrow( studyId: UUID ): Recruitment =
205278
participantRepository.getRecruitment( studyId )
206279
?: throw IllegalArgumentException( "Study with ID \"$studyId\" not found." )
280+
281+
private suspend fun getRecruitmentAndGroupByGroupIdOrThrow(
282+
groupId: UUID
283+
): Pair<Recruitment, StagedParticipantGroup>
284+
{
285+
for ( recruitment in participantRepository.getRecruitments() )
286+
{
287+
val participantGroup = recruitment.participantGroups[ groupId ]
288+
if ( participantGroup != null ) return recruitment to participantGroup
289+
}
290+
291+
throw IllegalArgumentException( "Study deployment with the specified groupId not found." )
292+
}
207293
}

carp.studies.core/src/commonMain/kotlin/dk/cachet/carp/studies/domain/users/ParticipantRepository.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ interface ParticipantRepository
2020
*/
2121
suspend fun getRecruitment( studyId: UUID ): Recruitment?
2222

23+
/**
24+
* Returns all stored [Recruitment]s.
25+
*/
26+
suspend fun getRecruitments(): List<Recruitment>
27+
2328
/**
2429
* Update a [Recruitment] which is already stored in this repository.
2530
*

carp.studies.core/src/commonMain/kotlin/dk/cachet/carp/studies/domain/users/Recruitment.kt

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,16 +215,34 @@ class Recruitment( val studyId: UUID, id: UUID = UUID.randomUUID(), createdOn: I
215215
val deploymentId = studyDeploymentStatus.studyDeploymentId
216216
val group: StagedParticipantGroup = requireNotNull( _participantGroups[ deploymentId ] )
217217
{ "A study deployment with ID \"$deploymentId\" is not part of this recruitment." }
218-
219-
val participants = group.participantIds.map { id -> _participants.first { it.id == id } }
218+
val participants = getParticipantsFor( group )
220219
return ParticipantGroupStatus.InDeployment.fromDeploymentStatus(
221-
participants.toSet(),
220+
participants,
222221
group.roleAssignments,
223222
studyDeploymentStatus,
224223
group.name
225224
)
226225
}
227226

227+
/**
228+
* Get the [ParticipantGroupStatus] for the staged participant [group].
229+
*
230+
* @throws IllegalArgumentException when the specified [group] is not part of this recruitment.
231+
*/
232+
fun getParticipantGroupStatus( group: StagedParticipantGroup ): ParticipantGroupStatus.Staged
233+
{
234+
val participants = getParticipantsFor( group )
235+
return ParticipantGroupStatus.Staged(
236+
id = group.id,
237+
participants = participants,
238+
assignedParticipantRoles = group.roleAssignments,
239+
name = group.name
240+
)
241+
}
242+
243+
private fun getParticipantsFor( group: StagedParticipantGroup ): Set<Participant> =
244+
group.participantIds.map { id -> _participants.first { it.id == id } }.toSet()
245+
228246
/**
229247
* Get an immutable snapshot of the current state of this [Recruitment] using the specified snapshot [version].
230248
*/

carp.studies.core/src/commonMain/kotlin/dk/cachet/carp/studies/infrastructure/InMemoryParticipantRepository.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ class InMemoryParticipantRepository : ParticipantRepository
2828
override suspend fun getRecruitment( studyId: UUID ): Recruitment? =
2929
recruitments[ studyId ]?.let { Recruitment.fromSnapshot( it ) }
3030

31+
/**
32+
* Returns all stored [Recruitment]s.
33+
*/
34+
override suspend fun getRecruitments(): List<Recruitment> =
35+
recruitments.values.map { Recruitment.fromSnapshot( it ) }
36+
3137
/**
3238
* Update a [Recruitment] which is already stored in this repository.
3339
*

carp.studies.core/src/commonMain/kotlin/dk/cachet/carp/studies/infrastructure/RecruitmentServiceDecorator.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,30 @@ class RecruitmentServiceDecorator(
3333
override suspend fun getParticipants( studyId: UUID ) =
3434
invoke( RecruitmentServiceRequest.GetParticipants( studyId ) )
3535

36+
@Deprecated(
37+
"Use CreateParticipantGroup and InviteParticipantGroup instead",
38+
ReplaceWith(
39+
"inviteParticipantGroup( createParticipantGroup( UUID.randomUUID(), group, studyId, name ).id )",
40+
"dk.cachet.carp.common.application.UUID"
41+
)
42+
)
43+
@Suppress( "DEPRECATION" )
3644
override suspend fun inviteNewParticipantGroup(
3745
studyId: UUID,
3846
group: Set<AssignedParticipantRoles>,
3947
name: String?
4048
) = invoke( RecruitmentServiceRequest.InviteNewParticipantGroup( studyId, group, name ) )
4149

50+
override suspend fun createParticipantGroup(
51+
groupId: UUID,
52+
group: Set<AssignedParticipantRoles>,
53+
studyId: UUID,
54+
name: String?
55+
) = invoke( RecruitmentServiceRequest.CreateParticipantGroup( groupId, group, studyId, name ) )
56+
57+
override suspend fun inviteParticipantGroup( groupId: UUID ) =
58+
invoke( RecruitmentServiceRequest.InviteParticipantGroup( groupId ) )
59+
4260
override suspend fun getParticipantGroupStatusList( studyId: UUID ) =
4361
invoke( RecruitmentServiceRequest.GetParticipantGroupStatusList( studyId ) )
4462

@@ -49,6 +67,7 @@ class RecruitmentServiceDecorator(
4967

5068
object RecruitmentServiceInvoker : ApplicationServiceInvoker<RecruitmentService, RecruitmentServiceRequest<*>>
5169
{
70+
@Suppress( "DEPRECATION" )
5271
override suspend fun RecruitmentServiceRequest<*>.invoke( service: RecruitmentService ): Any =
5372
when ( this )
5473
{
@@ -58,6 +77,9 @@ object RecruitmentServiceInvoker : ApplicationServiceInvoker<RecruitmentService,
5877
is RecruitmentServiceRequest.GetParticipants -> service.getParticipants( studyId )
5978
is RecruitmentServiceRequest.InviteNewParticipantGroup ->
6079
service.inviteNewParticipantGroup( studyId, group, name )
80+
is RecruitmentServiceRequest.CreateParticipantGroup ->
81+
service.createParticipantGroup(groupId, group, studyId, name )
82+
is RecruitmentServiceRequest.InviteParticipantGroup -> service.inviteParticipantGroup( groupId )
6183
is RecruitmentServiceRequest.GetParticipantGroupStatusList ->
6284
service.getParticipantGroupStatusList( studyId )
6385
is RecruitmentServiceRequest.StopParticipantGroup -> service.stopParticipantGroup( studyId, groupId )

carp.studies.core/src/commonMain/kotlin/dk/cachet/carp/studies/infrastructure/RecruitmentServiceRequest.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ sealed class RecruitmentServiceRequest<out TReturn> : ApplicationServiceRequest<
6565
}
6666

6767
@Serializable
68+
@Deprecated( "Use CreateParticipantGroup and InviteParticipantGroup instead." )
6869
data class InviteNewParticipantGroup(
6970
val studyId: UUID,
7071
val group: Set<AssignedParticipantRoles>,
@@ -75,6 +76,27 @@ sealed class RecruitmentServiceRequest<out TReturn> : ApplicationServiceRequest<
7576
override fun getResponseSerializer() = serializer<ParticipantGroupStatus>()
7677
}
7778

79+
@Serializable
80+
data class CreateParticipantGroup(
81+
val groupId: UUID,
82+
val group: Set<AssignedParticipantRoles>,
83+
val studyId: UUID,
84+
val name: String? = null
85+
) :
86+
RecruitmentServiceRequest<ParticipantGroupStatus>()
87+
{
88+
override fun getResponseSerializer() = serializer<ParticipantGroupStatus>()
89+
}
90+
91+
@Serializable
92+
data class InviteParticipantGroup(
93+
val groupId: UUID
94+
) :
95+
RecruitmentServiceRequest<ParticipantGroupStatus>()
96+
{
97+
override fun getResponseSerializer() = serializer<ParticipantGroupStatus>()
98+
}
99+
78100
@Serializable
79101
data class GetParticipantGroupStatusList( val studyId: UUID ) :
80102
RecruitmentServiceRequest<List<ParticipantGroupStatus>>()

carp.studies.core/src/commonTest/kotlin/dk/cachet/carp/studies/StudiesCodeSamples.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,13 @@ class StudiesCodeSamples
6161
val participation = AssignedParticipantRoles( participant.id, AssignedTo.All )
6262
val participantGroup = setOf( participation )
6363

64-
val groupStatus: ParticipantGroupStatus = recruitmentService.inviteNewParticipantGroup( studyId, participantGroup )
65-
val isInvited = groupStatus is ParticipantGroupStatus.Invited // True.
64+
val groupId = UUID.randomUUID()
65+
val stagedGroupStatus: ParticipantGroupStatus =
66+
recruitmentService.createParticipantGroup( groupId, participantGroup, studyId )
67+
val isStaged = stagedGroupStatus is ParticipantGroupStatus.Staged
68+
val invitedGroupStatus: ParticipantGroupStatus =
69+
recruitmentService.inviteParticipantGroup( groupId )
70+
val isInvited = invitedGroupStatus is ParticipantGroupStatus.Invited // True.
6671
}
6772
}
6873

carp.studies.core/src/commonTest/kotlin/dk/cachet/carp/studies/application/HostsIntegrationTest.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,12 @@ class HostsIntegrationTest
9696

9797
// Call succeeding means recruitment is ready for deployment.
9898
val assignRoles = setOf( AssignedParticipantRoles( participant.id, AssignedTo.All ) )
99-
recruitmentService.inviteNewParticipantGroup( study.studyId, assignRoles )
99+
val group = recruitmentService.createParticipantGroup(
100+
groupId = UUID.randomUUID(),
101+
group = assignRoles,
102+
study.studyId
103+
)
104+
recruitmentService.inviteParticipantGroup( group.id )
100105

101106
assertEquals( study.studyId, studyGoneLive?.study?.studyId )
102107
}
@@ -108,8 +113,13 @@ class HostsIntegrationTest
108113
// Add participant and deploy participant group.
109114
val participant = recruitmentService.addParticipant( studyId, EmailAddress( "test@test.com" ) )
110115
val assignRoles = AssignedParticipantRoles( participant.id, AssignedTo.All )
111-
val group = recruitmentService.inviteNewParticipantGroup( studyId, setOf( assignRoles ) )
112-
val deploymentId = group.id
116+
val stagedGroup = recruitmentService.createParticipantGroup(
117+
groupId = UUID.randomUUID(),
118+
group = setOf( assignRoles ),
119+
studyId
120+
)
121+
val invitedGroup = recruitmentService.inviteParticipantGroup( stagedGroup.id )
122+
val deploymentId = invitedGroup.id
113123

114124
var studyRemovedEvent: StudyService.Event.StudyRemoved? = null
115125
eventBus.registerHandler( StudyService::class, StudyService.Event.StudyRemoved::class, this ) { studyRemovedEvent = it }

0 commit comments

Comments
 (0)