Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4f6f3ef
MS-871 Add BiometricReferenceCreationEvent with boilerplate
luhmirin-s Feb 5, 2025
267f778
MS-871 Generalise reference ID creation to ensure it is consistent re…
luhmirin-s Feb 6, 2025
1d51771
MS-871 Remove ENROLMENT_V1 event definition, it has been long enough
luhmirin-s Feb 6, 2025
3f4ab3f
MS-871 Add ENROLMENT_V4 event implementation
luhmirin-s Feb 6, 2025
1abf5f4
MS-871 Track Enrolment v4 instead of v2
luhmirin-s Feb 6, 2025
9befa31
MS-871 Route probe biometric reference ID from capture to matcher
luhmirin-s Feb 6, 2025
0429c7c
MS-871 Add probe reference ID field to match events
luhmirin-s Feb 10, 2025
302f965
MS-871 Add biometric reference ID to sample models
luhmirin-s Feb 10, 2025
e3c5d91
MS-871 Deprecating PersonCreationEvent
luhmirin-s Feb 10, 2025
1e5029f
MS-871 Add EnrolmentRecordUpdate model and boilerplate
luhmirin-s Feb 5, 2025
e6ee157
MS-871 Handle record update down-sync event
luhmirin-s Feb 10, 2025
4a3580a
MS-871 Increase sync state database version to force destructive migr…
luhmirin-s Feb 10, 2025
99aca60
MS-871 Update SID version label
luhmirin-s Feb 10, 2025
b517052
MS-871 Fix double schedule of sync worker after successful login
luhmirin-s Feb 11, 2025
aad730f
MS-871 Remove outdates samples when Subject is updated in local database
luhmirin-s Feb 11, 2025
abf5263
MS-871 Allow reference creation be synced as biometric event
luhmirin-s Feb 12, 2025
c3b9797
MS-871 Move update logic into the realm transaction
luhmirin-s Feb 12, 2025
6f7d1ca
MS-871 Fix update event API model
luhmirin-s Feb 12, 2025
4fad11e
MS-871 Restore old SubjectAction name back to Creation
luhmirin-s Feb 12, 2025
cb971df
MS-871 Replace deterministic reference ID with random UUID
luhmirin-s Feb 12, 2025
ec02b36
MS-871 Fix naming and messaging in person creation references
luhmirin-s Mar 3, 2025
ad779e2
MS-871 Use all provided biometric references to create subject
luhmirin-s Mar 3, 2025
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
3 changes: 2 additions & 1 deletion build-logic/build_properties.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ extra.apply {
* Dev version >= 2024.2.1 is required for receiving biometric sdk age restrictions
* Dev version >= 2024.2.2 is required for float quality thresholds
* Dev version >= 2024.3.0 is required to receive configuration ID
* Dev version >= 2025.2.0 is required to support enrolment record updates
*/
set("VERSION_NAME", "2025.1.0")
set("VERSION_NAME", "2025.2.0")

/**
* Build type. The version code describes which build type was used for the build.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import java.io.Serializable

@Keep
data class FaceCaptureResult(
val referenceId: String,
val results: List<Item>,
) : Serializable {
@Keep
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject

Expand Down Expand Up @@ -184,8 +185,10 @@ internal class FaceCaptureViewModel @Inject constructor(
),
)
}
val referenceId = UUID.randomUUID().toString()
eventReporter.addBiometricReferenceCreationEvents(referenceId, items.mapNotNull { it.captureEventId })

_finishFlowEvent.send(FaceCaptureResult(items))
_finishFlowEvent.send(FaceCaptureResult(referenceId, items))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.simprints.core.tools.time.TimeHelper
import com.simprints.core.tools.time.Timestamp
import com.simprints.core.tools.utils.EncodingUtils
import com.simprints.face.capture.models.FaceDetection
import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent
import com.simprints.infra.events.event.domain.models.face.FaceCaptureBiometricsEvent
import com.simprints.infra.events.event.domain.models.face.FaceCaptureConfirmationEvent
import com.simprints.infra.events.event.domain.models.face.FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload.Result
Expand Down Expand Up @@ -100,4 +101,18 @@ internal class SimpleCaptureEventReporter @Inject constructor(
it.format,
)
}!!

fun addBiometricReferenceCreationEvents(
referenceId: String,
captureIds: List<String>,
) = sessionCoroutineScope.launch {
eventRepository.addOrUpdateEvent(
BiometricReferenceCreationEvent(
startTime = timeHelper.now(),
referenceId = referenceId,
modality = BiometricReferenceCreationEvent.BiometricReferenceModality.FACE,
captureIds = captureIds,
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ class FaceCaptureViewModelTest {
coVerify(atLeast = 1) { faceImageUseCase.invoke(any(), any()) }
}

@Test
fun `Save biometric reference creation when flow finishes`() {
viewModel.captureFinished(faceDetections)
viewModel.flowFinished()
coVerify(atLeast = 1) {
eventReporter.addBiometricReferenceCreationEvents(any(), any())
}
}

@Test
fun `Recapture requests clears capture list`() {
viewModel.captureFinished(faceDetections)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import com.simprints.core.tools.time.Timestamp
import com.simprints.core.tools.utils.EncodingUtils
import com.simprints.face.capture.models.FaceDetection
import com.simprints.face.infra.basebiosdk.detection.Face
import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent
import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent.BiometricReferenceCreationPayload
import com.simprints.infra.events.event.domain.models.face.FaceCaptureBiometricsEvent
import com.simprints.infra.events.event.domain.models.face.FaceCaptureConfirmationEvent
import com.simprints.infra.events.event.domain.models.face.FaceCaptureEvent
Expand Down Expand Up @@ -73,8 +75,9 @@ class SimpleCaptureEventReporterTest {
eventRepository.addOrUpdateEvent(
withArg {
assertThat(it).isInstanceOf(FaceCaptureConfirmationEvent::class.java)
assertThat((it.payload as FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload).result)
.isEqualTo(FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload.Result.CONTINUE)
assertThat((it.payload as FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload).result).isEqualTo(
FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload.Result.CONTINUE,
)
},
)
}
Expand All @@ -87,8 +90,9 @@ class SimpleCaptureEventReporterTest {
eventRepository.addOrUpdateEvent(
withArg {
assertThat(it).isInstanceOf(FaceCaptureConfirmationEvent::class.java)
assertThat((it.payload as FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload).result)
.isEqualTo(FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload.Result.RECAPTURE)
assertThat((it.payload as FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload).result).isEqualTo(
FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload.Result.RECAPTURE,
)
},
)
}
Expand Down Expand Up @@ -182,6 +186,20 @@ class SimpleCaptureEventReporterTest {
}
}

@Test
fun `Adds reference creation event`() = runTest {
reporter.addBiometricReferenceCreationEvents("id", listOf("id1", "id2", "id3"))
coVerify {
eventRepository.addOrUpdateEvent(
withArg {
assertThat(it).isInstanceOf(BiometricReferenceCreationEvent::class.java)
assertThat((it.payload as BiometricReferenceCreationPayload).modality)
.isEqualTo(BiometricReferenceCreationEvent.BiometricReferenceModality.FACE)
},
)
}
}

private fun getDetection(status: FaceDetection.Status) =
FaceDetection(mockk(), getFace(), status, mockk(), Timestamp(1L), false, "id", Timestamp(1L))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ sealed class EnrolLastBiometricStepResult : Parcelable {
@Keep
@Parcelize
data class FingerprintCaptureResult(
val referenceId: String,
val results: List<FingerTemplateCaptureResult>,
) : EnrolLastBiometricStepResult()

@Keep
@Parcelize
data class FaceCaptureResult(
val referenceId: String,
val results: List<FaceTemplateCaptureResult>,
) : EnrolLastBiometricStepResult()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import com.simprints.infra.config.sync.ConfigManager
import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository
import com.simprints.infra.enrolment.records.repository.domain.models.Subject
import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAction
import com.simprints.infra.events.event.domain.models.EnrolmentEventV2
import com.simprints.infra.events.event.domain.models.PersonCreationEvent
import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent
import com.simprints.infra.events.event.domain.models.EnrolmentEventV4
import com.simprints.infra.events.session.SessionEventRepository
import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ENROLMENT
import com.simprints.infra.logging.Simber
Expand Down Expand Up @@ -85,20 +85,20 @@ internal class EnrolLastBiometricViewModel @Inject constructor(
private suspend fun registerEvent(subject: Subject) {
Simber.d("Register events for enrolments", tag = ENROLMENT)

val personCreationEvent = eventRepository
val biometricReferenceIds = eventRepository
.getEventsInCurrentSession()
.filterIsInstance<PersonCreationEvent>()
.filterIsInstance<BiometricReferenceCreationEvent>()
.sortedByDescending { it.payload.createdAt }
.first()
.map { it.payload.id }

eventRepository.addOrUpdateEvent(
EnrolmentEventV2(
EnrolmentEventV4(
timeHelper.now(),
subject.subjectId,
subject.projectId,
subject.moduleId,
subject.attendantId,
personCreationEvent.id,
biometricReferenceIds,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,30 @@ internal class BuildSubjectUseCase @Inject constructor(
params.moduleId,
createdAt = Date(timeHelper.now().ms),
fingerprintSamples = getFingerprintCaptureResult(params.steps)
?.map(::fingerprintSample)
?.let { result -> result.results.map { fingerprintSample(result.referenceId, it) } }
.orEmpty(),
faceSamples = getFaceCaptureResult(params.steps)
?.let { result -> result.results.map { faceSample(result.referenceId, it) } }
.orEmpty(),
faceSamples = getFaceCaptureResult(params.steps)?.map(::faceSample).orEmpty(),
)

private fun getFingerprintCaptureResult(steps: List<EnrolLastBiometricStepResult>) = steps
.filterIsInstance<EnrolLastBiometricStepResult.FingerprintCaptureResult>()
.firstOrNull()
?.results

private fun getFaceCaptureResult(steps: List<EnrolLastBiometricStepResult>) = steps
.filterIsInstance<EnrolLastBiometricStepResult.FaceCaptureResult>()
.firstOrNull()
?.results

private fun fingerprintSample(it: FingerTemplateCaptureResult) = FingerprintSample(
fromDomainToModuleApi(it.finger),
it.template,
it.templateQualityScore,
it.format,
private fun fingerprintSample(
referenceId: String,
result: FingerTemplateCaptureResult,
) = FingerprintSample(
fromDomainToModuleApi(result.finger),
result.template,
result.templateQualityScore,
result.format,
referenceId,
)

private fun fromDomainToModuleApi(finger: Finger) = when (finger) {
Expand All @@ -61,5 +65,8 @@ internal class BuildSubjectUseCase @Inject constructor(
Finger.LEFT_5TH_FINGER -> IFingerIdentifier.LEFT_5TH_FINGER
}

private fun faceSample(it: FaceTemplateCaptureResult) = FaceSample(it.template, it.format)
private fun faceSample(
referenceId: String,
result: FaceTemplateCaptureResult,
) = FaceSample(result.template, result.format, referenceId)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import com.simprints.infra.config.store.models.ProjectConfiguration
import com.simprints.infra.config.sync.ConfigManager
import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository
import com.simprints.infra.enrolment.records.repository.domain.models.Subject
import com.simprints.infra.events.event.domain.models.EnrolmentEventV2
import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent
import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent.BiometricReferenceCreationPayload
import com.simprints.infra.events.event.domain.models.EnrolmentEventV4
import com.simprints.infra.events.event.domain.models.PersonCreationEvent
import com.simprints.infra.events.event.domain.models.PersonCreationEvent.PersonCreationPayload
import com.simprints.infra.events.session.SessionEventRepository
import com.simprints.testtools.common.coroutines.TestCoroutineRule
import io.mockk.MockKAnnotations
Expand Down Expand Up @@ -213,30 +214,36 @@ internal class EnrolLastBiometricViewModelTest {
}

@Test
fun `Uses latest PersonCreationEvent for Enrolment event`() = runTest {
val personCreationEvent1 = mockk<PersonCreationEvent> {
every { id } returns "personCreationEventId1"
every { payload } returns mockk<PersonCreationPayload> {
fun `Uses all BiometricReferenceCreationEvent for Enrolment event`() = runTest {
val biometricReferenceCreationEvent1 = mockk<BiometricReferenceCreationEvent> {
every { id } returns "biometricReferenceCreationEventId1"
every { payload } returns mockk<BiometricReferenceCreationPayload> {
every { createdAt } returns Timestamp(1)
every { id } returns "referenceId1"
}
}
val personCreationId2 = "personCreationEventId2"
val personCreationEvent2 = mockk<PersonCreationEvent> {
every { id } returns personCreationId2
every { payload } returns mockk<PersonCreationPayload> {
val biometricReferenceCreationEvent2 = mockk<BiometricReferenceCreationEvent> {
every { id } returns "biometricReferenceCreationEventId2"
every { payload } returns mockk<BiometricReferenceCreationPayload> {
every { createdAt } returns Timestamp(2)
every { id } returns "referenceId2"
}
}

coEvery { eventRepository.getEventsInCurrentSession() } returns listOf(
personCreationEvent1,
personCreationEvent2,
biometricReferenceCreationEvent2,
biometricReferenceCreationEvent1,
)

viewModel.enrolBiometric(createParams(listOf()))

coVerify {
eventRepository.addOrUpdateEvent(
match { it is EnrolmentEventV2 && it.payload.personCreationEventId == personCreationId2 },
withArg {
assertThat(it).isInstanceOf(EnrolmentEventV4::class.java)
assertThat((it.payload as EnrolmentEventV4.EnrolmentPayload).biometricReferenceIds)
.containsExactly("referenceId1", "referenceId2")
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,14 @@ class BuildSubjectUseCaseTest {
createParams(
listOf(
EnrolLastBiometricStepResult.FingerprintMatchResult(emptyList(), mockk()),
EnrolLastBiometricStepResult.FingerprintCaptureResult(listOf(mockFingerprintResults(Finger.RIGHT_THUMB))),
EnrolLastBiometricStepResult.FingerprintCaptureResult(listOf(mockFingerprintResults(Finger.LEFT_THUMB))),
EnrolLastBiometricStepResult.FingerprintCaptureResult(
REFERENCE_ID,
listOf(mockFingerprintResults(Finger.RIGHT_THUMB)),
),
EnrolLastBiometricStepResult.FingerprintCaptureResult(
REFERENCE_ID,
listOf(mockFingerprintResults(Finger.LEFT_THUMB)),
),
),
),
)
Expand All @@ -84,6 +90,7 @@ class BuildSubjectUseCaseTest {
createParams(
listOf(
EnrolLastBiometricStepResult.FingerprintCaptureResult(
REFERENCE_ID,
listOf(
mockFingerprintResults(Finger.RIGHT_5TH_FINGER),
mockFingerprintResults(Finger.RIGHT_4TH_FINGER),
Expand Down Expand Up @@ -111,8 +118,8 @@ class BuildSubjectUseCaseTest {
createParams(
listOf(
EnrolLastBiometricStepResult.FaceMatchResult(emptyList()),
EnrolLastBiometricStepResult.FaceCaptureResult(mockFaceResultsList("first")),
EnrolLastBiometricStepResult.FaceCaptureResult(mockFaceResultsList("second")),
EnrolLastBiometricStepResult.FaceCaptureResult(REFERENCE_ID, mockFaceResultsList("first")),
EnrolLastBiometricStepResult.FaceCaptureResult(REFERENCE_ID, mockFaceResultsList("second")),
),
),
)
Expand All @@ -133,6 +140,8 @@ class BuildSubjectUseCaseTest {
private fun mockFaceResultsList(format: String) = listOf(FaceTemplateCaptureResult(byteArrayOf(), format))

companion object {
private const val REFERENCE_ID = "referenceId"

private const val PROJECT_ID = "projectId"
private val USER_ID = "userId".asTokenizableRaw()
private val MODULE_ID = "moduleId".asTokenizableRaw()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ internal class StartBackgroundSyncUseCase @Inject constructor(
private val configManager: ConfigManager,
) {
suspend operator fun invoke() {
syncOrchestrator.scheduleBackgroundWork()

val frequency = configManager.getProjectConfiguration().synchronization.frequency
if (frequency == SynchronizationConfiguration.Frequency.PERIODICALLY_AND_ON_SESSION_START) {
syncOrchestrator.startEventSync()
}

syncOrchestrator.scheduleBackgroundWork(
withDelay = frequency != SynchronizationConfiguration.Frequency.PERIODICALLY_AND_ON_SESSION_START,
)
Comment thread
luhmirin-s marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
Expand Down Expand Up @@ -38,9 +37,7 @@ class StartBackgroundSyncUseCaseTest {

useCase.invoke()

coVerify {
syncOrchestrator.scheduleBackgroundWork()
}
coVerify { syncOrchestrator.scheduleBackgroundWork(any()) }
}

@Test
Expand All @@ -50,7 +47,7 @@ class StartBackgroundSyncUseCaseTest {

useCase.invoke()

verify { syncOrchestrator.startEventSync() }
coVerify { syncOrchestrator.scheduleBackgroundWork(eq(false)) }
}

@Test
Expand All @@ -60,6 +57,6 @@ class StartBackgroundSyncUseCaseTest {

useCase.invoke()

verify(exactly = 0) { syncOrchestrator.startEventSync() }
coVerify { syncOrchestrator.scheduleBackgroundWork(eq(true)) }
}
}
Loading