diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml index 94623f25a..d4ccd6e05 100644 --- a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml +++ b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml @@ -17,7 +17,7 @@ onFlowComplete: file: "scripts/teardown.js" --- -# Invite new user: +# Invite Teacher as new user: - runFlow: "subflows/school_admin_login_flow.yaml" - assertVisible: id: "app_title" @@ -96,6 +96,10 @@ onFlowComplete: - tapOn: "Password*" - inputText: "test123" - tapOn: "Sign-up" +- runFlow: + when: + visible: "Save password for Respect?" + file: "subflows/save_password_prompt_cancel.yaml" - assertVisible: id: "app_title" text: "Apps" @@ -118,7 +122,7 @@ onFlowComplete: - copyTextFrom: id: "invite_code" -# C) Student sign-up via invite code to the class +# Student sign-up via invite code to the class - clearState: world.respect.app - launchApp: arguments: @@ -173,23 +177,14 @@ onFlowComplete: text: "Waiting for approval" - assertVisible: "Please wait" -# H) Teacher approve student's request to join the class +# Teacher approve student's request to join the class - clearState: world.respect.app -- launchApp: - arguments: - respect_directory: ${output.SCHOOL_URL} -- tapOn: "Get Started" - runFlow: - file: "subflows/get_started_select_school_by_name.yaml" + file: "subflows/school_user_login_flow.yaml" env: SCHOOL_NAME: ${SCHOOL_NAME} -- tapOn: - id: "username" -- inputText: "teacherauser" -- tapOn: - id : "password" -- inputText: "test123" -- tapOn: "Login" + USER_NAME: "teacherauser" + USER_PASSWORD: "test123" - assertVisible: "Apps" - tapOn: "Classes" - assertVisible: @@ -199,15 +194,25 @@ onFlowComplete: - assertVisible: "Pending requests.*" - assertVisible: "Student User.*" - tapOn: "Accept Invite" +- assertNotVisible: "Pending requests.*" -# E) Teacher share invite link for parent device based signup -- tapOn: "Add student" +# Teacher send family member invite to Parent User +- tapOn: "Student User" +- assertVisible: + id: "app_title" + text: "Student User" +- tapOn: + id: "floating_action_button" +- assertVisible: + id: "app_title" + text: "Edit person" +- tapOn: "Family member" - tapOn: "Invite person" -- tapOn: "Invite via parents" +- assertVisible: "Student User / Family member" - tapOn: "Approval required" # turn the switch off - assertVisible: "Approval not required until:.*" - copyTextFrom: - id: "invite_url" + id: "invite_url" # Parent sign-up to the app using invite link - runFlow: @@ -220,10 +225,10 @@ onFlowComplete: text: "Invitation" - assertVisible: "Role" - assertVisible: "Parent" +- assertVisible: "Child" +- assertVisible: "Student User" - assertVisible: "School name" - assertVisible: ${output.SCHOOL_NAME} -- assertVisible: "Class name" -- assertVisible: "New Class" - assertVisible: "School server URL" - assertVisible: ${output.SCHOOL_URL} - tapOn: "Next" @@ -254,13 +259,13 @@ onFlowComplete: - tapOn: "Password*" - inputText: "test123" - tapOn: "Sign-up" -- tapOn: "Child's name*" -- inputText: "Student User2" -- tapOn: "Gender*" -- tapOn: "Female" -- tapOn: "Child's date of birth*" -- runScript: - file: "scripts/setDate.js" -- inputText: ${output.pastYearDateC} -- tapOn: "Done" -# 1/Feb/2026: MD: Maestro AGAIN does not actually deliver the tap. Thanks. +- runFlow: + when: + visible: "Save password for Respect?" + file: "subflows/save_password_prompt_cancel.yaml" +- assertVisible: + id: "app_title" + text: "Apps" +- tapOn: "People" +- tapOn: "Student User" +- assertVisible: "Parent User" \ No newline at end of file diff --git a/.maestro/flows/subflows/school_user_login_flow.yaml b/.maestro/flows/subflows/school_user_login_flow.yaml new file mode 100644 index 000000000..c7c2cda88 --- /dev/null +++ b/.maestro/flows/subflows/school_user_login_flow.yaml @@ -0,0 +1,26 @@ +appId: world.respect.app + +--- +- launchApp: + arguments: + respect_directory: ${output.SCHOOL_URL} + +- tapOn: "Get Started" + +- runFlow: + file: "get_started_select_school_by_name.yaml" + env: + SCHOOL_NAME: ${SCHOOL_NAME} + +- tapOn: + id: "username" +- inputText: ${USER_NAME} +- tapOn: + id : "password" +- inputText: ${USER_PASSWORD} +- tapOn: "Login" +- runFlow: + when: + visible: "Save password for Respect?" + file: "save_password_prompt_cancel.yaml" + diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/manageuser/acceptinvite/AcceptInviteScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/manageuser/acceptinvite/AcceptInviteScreen.kt index 1c47c2a89..e67c4a70a 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/manageuser/acceptinvite/AcceptInviteScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/manageuser/acceptinvite/AcceptInviteScreen.kt @@ -25,6 +25,7 @@ import world.respect.app.components.uiTextStringResource import world.respect.datalayer.school.model.ClassInvite import world.respect.datalayer.school.model.NewUserInvite import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.child import world.respect.shared.generated.resources.class_name import world.respect.shared.generated.resources.loading import world.respect.shared.generated.resources.next @@ -122,7 +123,16 @@ fun AcceptInviteScreen( } else -> { - //Do nothing else + RespectDetailField( + modifier = Modifier.defaultItemPadding(), + label = { Text(stringResource(Res.string.role)) }, + value = { Text(stringResource(invite.roleLabel)) } + ) + RespectDetailField( + modifier = Modifier.defaultItemPadding(), + label = { Text(stringResource(Res.string.child)) }, + value = { Text(uiState.inviteInfo?.childName?:"") } + ) } } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/person/inviteperson/InvitePersonScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/person/inviteperson/InvitePersonScreen.kt index 1abf71dfd..2791d1128 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/person/inviteperson/InvitePersonScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/person/inviteperson/InvitePersonScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectable @@ -50,6 +51,7 @@ import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.approval_not_required_until import world.respect.shared.generated.resources.approval_required import world.respect.shared.generated.resources.copy_link +import world.respect.shared.generated.resources.family_member import world.respect.shared.generated.resources.invite_code_label import world.respect.shared.generated.resources.invite_students_directly import world.respect.shared.generated.resources.invite_via_email @@ -135,6 +137,15 @@ fun InvitePersonScreen( uiState.inviteUrl?.also { link -> val linkStr = link.toString() + uiState.childName?.let { + Text( + text = "$it / "+stringResource(Res.string.family_member), + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.CenterHorizontally) + .padding(vertical = 8.dp), + ) + } + Image( painter = rememberQrCodePainter(linkStr), contentDescription = stringResource(Res.string.qr_code), diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/InviteDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/InviteDataSourceDb.kt index 1cd7ce281..0f8436b40 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/InviteDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/InviteDataSourceDb.kt @@ -16,6 +16,7 @@ import world.respect.datalayer.db.school.adapters.toModel import world.respect.datalayer.exceptions.ForbiddenException import world.respect.datalayer.school.InviteDataSource import world.respect.datalayer.school.InviteDataSourceLocal +import world.respect.datalayer.school.PersonDataSourceLocal import world.respect.datalayer.school.domain.CheckPersonPermissionUseCase import world.respect.datalayer.school.model.ClassInvite import world.respect.datalayer.school.model.EnrollmentRoleEnum @@ -114,7 +115,23 @@ class InviteDataSourceDb( else -> { //Family member invite - val knownPersonUid = (inviteToStore as? FamilyMemberInvite)?.personUid ?: "0" + val knownPersonUid = + (inviteToStore as? FamilyMemberInvite)?.personUid ?: "0" + val authGuid = authenticatedUser.guid + + if (authGuid != knownPersonUid) { + val hasWrite = checkPersonPermissionUseCase( + otherPersonUid = knownPersonUid, + otherPersonKnownRole = null, + permissionsRequiredByRole = CheckPersonPermissionUseCase.PermissionsRequiredByRole.WRITE_PERMISSIONS + ) + + if (!hasWrite) { + throw Exception( + "user does not have permission to create family invite for $knownPersonUid" + ) + } + } } } } diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/respect/model/invite/RespectInviteInfo.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/respect/model/invite/RespectInviteInfo.kt index 33e911dcd..08048ce35 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/respect/model/invite/RespectInviteInfo.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/respect/model/invite/RespectInviteInfo.kt @@ -13,6 +13,7 @@ import world.respect.datalayer.school.model.Invite2 class RespectInviteInfo( val classGuid: String?=null, val className: String?=null, + val childName: String?=null, val userInviteType: UserInviteType?=null, val invite: Invite2? = null ) { diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/Invite.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/Invite.kt index 5df9eab25..50542a5e3 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/Invite.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/Invite.kt @@ -120,10 +120,15 @@ data class ClassInvite( data class FamilyMemberInvite( override val uid: String, override val code: String, - override val approvalRequiredAfter: InstantAsISO8601, + override val approvalRequiredAfter: InstantAsISO8601= Instant.fromEpochMilliseconds(0), override val lastModified: InstantAsISO8601 = Clock.System.now(), override val stored: InstantAsISO8601 = Clock.System.now(), override val status: StatusEnum = StatusEnum.ACTIVE, val personUid: String, -): Invite2 +): Invite2 { + companion object { + fun uidFor(personUid: String?): String = "$TYPE_FAMILY_MEMBER:$personUid" + } +} + diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 52b9cb015..768f10c64 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -498,6 +498,7 @@ System administrator Site administrator Role + Child Date Time diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 17746057e..fbdb61de5 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -837,6 +837,11 @@ data class InvitePerson( val inviteUid: String, ): InvitePersonOptions + @Serializable + @SerialName("family") + data class FamilyInviteOptions( + val personUid: String + ) : InvitePersonOptions @Transient val invitePersonOptions: InvitePersonOptions = Json.decodeFromString( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/edit/PersonEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/edit/PersonEditViewModel.kt index 5ee68fb28..5c45e7129 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/edit/PersonEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/edit/PersonEditViewModel.kt @@ -56,6 +56,10 @@ import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState import kotlin.collections.first import kotlin.getValue import kotlin.time.Clock +import world.respect.datalayer.school.model.FamilyMemberInvite +import world.respect.datalayer.school.model.Invite2 +import world.respect.datalayer.school.model.StatusEnum +import kotlin.time.Duration.Companion.minutes data class PersonEditUiState( val uid: String = "", @@ -298,12 +302,12 @@ class PersonEditViewModel( NavCommand.Navigate( PersonList.create( filterByRole = filterByRole, - personGuid = route.guid, + personGuid = FamilyMemberInvite.uidFor(route.guid), resultDest = RouteResultDest( resultPopUpTo = route, resultKey = PERSON_SELECT_RESULT ), - hideInvite = true, + hideInvite = false, ) ) ) @@ -392,6 +396,19 @@ class PersonEditViewModel( familyMembersAdded + familyMembersRemoved + personToSave.copy(lastModified = modTime) ) + if (route.guid == null && personToSave.roles.any { it.roleEnum == PersonRoleEnum.STUDENT }) { + val invite = FamilyMemberInvite( + uid = FamilyMemberInvite.uidFor(personToSave.guid), + code = Invite2.newRandomCode(), + approvalRequiredAfter = modTime + Invite2.APPROVAL_NOT_REQUIRED_INTERVAL_MINS.minutes, + lastModified = modTime, + stored = modTime, + status = StatusEnum.ACTIVE, + personUid = personToSave.guid + ) + schoolDataSource.inviteDataSource.store(listOf(invite)) + } + if( !navResultReturner.sendResultIfResultExpected( route = route, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/inviteperson/InvitePersonViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/inviteperson/InvitePersonViewModel.kt index e386d7d54..4f7e62f2a 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/inviteperson/InvitePersonViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/inviteperson/InvitePersonViewModel.kt @@ -22,6 +22,7 @@ import world.respect.datalayer.DataLoadState import world.respect.datalayer.DataLoadingState import world.respect.datalayer.NoDataLoadedState import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.db.school.ext.fullName import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.school.domain.GetWritableRolesListUseCase import world.respect.datalayer.school.ext.copyInvite @@ -30,6 +31,7 @@ import world.respect.datalayer.school.ext.newUserInviteUid import world.respect.datalayer.school.model.ClassInvite import world.respect.datalayer.school.model.ClassInviteModeEnum import world.respect.datalayer.school.model.EnrollmentRoleEnum +import world.respect.datalayer.school.model.FamilyMemberInvite import world.respect.datalayer.school.model.Invite2 import world.respect.datalayer.school.model.NewUserInvite import world.respect.datalayer.school.model.PersonRoleEnum @@ -62,6 +64,7 @@ data class InvitePersonUiState( val selectedRole: PersonRoleEnum? = null, val className: String? = null, val schoolName: String? = null, + val childName: String? = null, val roleOptions: List = emptyList() ) { val inviteCode: String? @@ -132,6 +135,7 @@ class InvitePersonViewModel( } _inviteUid.value = (route.invitePersonOptions as? InvitePerson.ClassInviteOptions)?.inviteUid + ?: (route.invitePersonOptions as? InvitePerson.FamilyInviteOptions)?.personUid ?: selectedRole.newUserInviteUid _inviteUid.collectLatest { inviteUid -> @@ -140,6 +144,17 @@ class InvitePersonViewModel( uid = inviteUid, loadParams = DataLoadParams() ).collectLatest { invite -> + (invite.dataOrNull() as? FamilyMemberInvite)?.let { familyInvite -> + val childPerson= schoolDataSource.personDataSource.findByGuid( + loadParams = DataLoadParams(), + guid = familyInvite.personUid + ).dataOrNull() + _uiState.update { prev -> + prev.copy( + childName = childPerson?.fullName(), + ) + } + } _uiState.update { prev -> prev.copy( invite = invite, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/list/PersonListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/list/PersonListViewModel.kt index 64ee9fced..92b5cf7fc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/list/PersonListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/person/list/PersonListViewModel.kt @@ -38,6 +38,7 @@ import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.AppBarSearchUiState import world.respect.datalayer.school.domain.CheckPersonPermissionUseCase.PermissionsRequiredByRole import world.respect.datalayer.school.model.Person +import world.respect.datalayer.school.model.PersonRoleEnum import world.respect.datalayer.school.model.PersonStatusEnum import world.respect.shared.domain.account.invite.ApproveOrDeclineInviteRequestUseCase import world.respect.shared.domain.permissions.CheckSchoolPermissionsUseCase @@ -240,19 +241,29 @@ class PersonListViewModel( } fun onClickInvitePerson() { + val inviteOptions = when { + route.inviteUid != null -> { + InvitePerson.ClassInviteOptions( + inviteUid = route.inviteUid + ) + } + + route.filterByRole == PersonRoleEnum.PARENT && route.personGuidStr != null -> { + InvitePerson.FamilyInviteOptions( + personUid = route.personGuidStr + ) + } + + else -> { + InvitePerson.NewUserInviteOptions( + presetRole = route.filterByRole + ) + } + } + _navCommandFlow.tryEmit( NavCommand.Navigate( - InvitePerson.create( - invitePersonOptions = if(route.inviteUid != null) { - InvitePerson.ClassInviteOptions( - inviteUid = route.inviteUid - ) - }else { - InvitePerson.NewUserInviteOptions( - presetRole = route.filterByRole - ) - } - ) + InvitePerson.create(invitePersonOptions = inviteOptions) ) ) } diff --git a/respect-lib-shared/src/jvmMain/kotlin/world/respect/shared/domain/account/invite/RedeemInviteUseCaseDb.kt b/respect-lib-shared/src/jvmMain/kotlin/world/respect/shared/domain/account/invite/RedeemInviteUseCaseDb.kt index b294e00e6..6e4832f9f 100644 --- a/respect-lib-shared/src/jvmMain/kotlin/world/respect/shared/domain/account/invite/RedeemInviteUseCaseDb.kt +++ b/respect-lib-shared/src/jvmMain/kotlin/world/respect/shared/domain/account/invite/RedeemInviteUseCaseDb.kt @@ -12,11 +12,13 @@ import world.respect.credentials.passkey.RespectQRBadgeCredential import world.respect.credentials.passkey.RespectUserHandle import world.respect.credentials.passkey.request.GetPasskeyProviderInfoUseCase import world.respect.datalayer.AuthenticatedUserPrincipalId +import world.respect.datalayer.DataLoadParams import world.respect.datalayer.SchoolDataSourceLocal import world.respect.datalayer.UidNumberMapper import world.respect.datalayer.db.RespectSchoolDatabase import world.respect.datalayer.db.school.adapters.toEntity import world.respect.datalayer.db.school.adapters.toModel +import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.school.adapters.toPersonPasskey import world.respect.datalayer.school.ext.accepterEnrollmentRole import world.respect.datalayer.school.ext.accepterPersonRole @@ -28,6 +30,8 @@ import world.respect.datalayer.school.model.NewUserInvite import world.respect.datalayer.school.model.ClassInvite import world.respect.datalayer.school.model.ClassInviteModeEnum import world.respect.datalayer.school.model.Enrollment +import world.respect.datalayer.school.model.FamilyMemberInvite +import world.respect.datalayer.school.model.PersonRoleEnum import world.respect.datalayer.school.model.PersonStatusEnum import world.respect.datalayer.school.model.StatusEnum import world.respect.libutil.ext.randomString @@ -115,7 +119,42 @@ class RedeemInviteUseCaseDb( ) ) } + val timeNow = Clock.System.now() + if (inviteFromDb is FamilyMemberInvite) { + val parentUid = inviteFromDb.personUid + + val inviterPerson = schoolDataSourceVal.personDataSource.findByGuid( + loadParams = DataLoadParams(), + guid = parentUid + ).dataOrNull() ?: throw IllegalStateException("person not found: $parentUid") + + + val updatedInviter = inviterPerson.copy( + relatedPersonUids = inviterPerson.relatedPersonUids + accountPerson.guid, + lastModified = timeNow + ) + + val updatedAccountPerson = accountPerson.copy( + relatedPersonUids = accountPerson.relatedPersonUids + parentUid, + lastModified = timeNow + ) + schoolDataSourceVal.personDataSource.updateLocal(listOf(updatedInviter, updatedAccountPerson)) + } + if (inviteFromDb !is FamilyMemberInvite){ + if ( accountPerson.roles.any { it.roleEnum == PersonRoleEnum.STUDENT }) { + val invite = FamilyMemberInvite( + uid = FamilyMemberInvite.uidFor(accountPerson.guid), + code = Invite2.newRandomCode(), + approvalRequiredAfter = timeNow, + lastModified = timeNow, + stored = timeNow, + status = StatusEnum.ACTIVE, + personUid = accountPerson.guid + ) + schoolDataSourceVal.inviteDataSource.store(listOf(invite)) + } + } val credential = redeemRequest.account.credential val authResponse = when (credential) { diff --git a/respect-server/src/main/kotlin/world/respect/server/account/invite/GetInviteInfoUseCaseServer.kt b/respect-server/src/main/kotlin/world/respect/server/account/invite/GetInviteInfoUseCaseServer.kt index 2135308db..893a9e36c 100644 --- a/respect-server/src/main/kotlin/world/respect/server/account/invite/GetInviteInfoUseCaseServer.kt +++ b/respect-server/src/main/kotlin/world/respect/server/account/invite/GetInviteInfoUseCaseServer.kt @@ -26,9 +26,24 @@ class GetInviteInfoUseCaseServer( }else { null } - + val childUid = invite.iForFamilyOfGuid + val childName = if(childUid != null) { + val child = schoolDb.getPersonEntityDao().findByGuidNum(uidNumberMapper(childUid)) + buildString { + append(child?.person?.pGivenName) + append(" ") + child?.person?.pMiddleName?.also { + append(it) + append(" ") + } + append(child?.person?.pFamilyName) + } + }else { + null + } return RespectInviteInfo( className = className, + childName = childName, invite = invite.toModel(), ) }