diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/ConfigStoreModule.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/ConfigStoreModule.kt index f223eb2563..66bd506c38 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/ConfigStoreModule.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/ConfigStoreModule.kt @@ -14,6 +14,7 @@ import com.simprints.infra.config.store.local.migrations.ProjectConfigFingerprin import com.simprints.infra.config.store.local.migrations.ProjectConfigLedsModeMigration import com.simprints.infra.config.store.local.migrations.ProjectConfigMatchingModalitiesMigration import com.simprints.infra.config.store.local.migrations.ProjectConfigQualityThresholdMigration +import com.simprints.infra.config.store.local.migrations.ProjectConfigSampleUploadMigration import com.simprints.infra.config.store.local.migrations.ProjectConfigSharedPrefsMigration import com.simprints.infra.config.store.local.migrations.ProjectRealmMigration import com.simprints.infra.config.store.local.models.ProtoDeviceConfiguration @@ -76,6 +77,7 @@ object DataStoreModule { projectConfigLedsModeMigration: ProjectConfigLedsModeMigration, projectConfigMatchingModalitiesMigration: ProjectConfigMatchingModalitiesMigration, projectConfigFaceEmptyVersionMigration: ProjectConfigFaceEmptyVersionMigration, + projectConfigSampleUploadMigration: ProjectConfigSampleUploadMigration, ): DataStore = DataStoreFactory.create( serializer = ProjectConfigurationSerializer, produceFile = { appContext.dataStoreFile(PROJECT_CONFIG_DATA_STORE_FILE_NAME) }, @@ -88,6 +90,7 @@ object DataStoreModule { projectConfigLedsModeMigration, projectConfigMatchingModalitiesMigration, projectConfigFaceEmptyVersionMigration, + projectConfigSampleUploadMigration, ), ) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt index 7f5969f47d..13cce83684 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt @@ -20,6 +20,7 @@ import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.config.store.models.IdentificationConfiguration import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.ProjectConfiguration +import com.simprints.infra.config.store.models.SampleSynchronizationConfiguration import com.simprints.infra.config.store.models.SettingsPasswordConfig import com.simprints.infra.config.store.models.SynchronizationConfiguration import com.simprints.infra.config.store.models.TokenKeyType @@ -37,7 +38,7 @@ internal class ConfigLocalDataSourceImpl @Inject constructor( private val projectDataStore: DataStore, private val configDataStore: DataStore, private val deviceConfigDataStore: DataStore, - private val tokenizationProcessor: TokenizationProcessor + private val tokenizationProcessor: TokenizationProcessor, ) : ConfigLocalDataSource { override suspend fun saveProject(project: Project) { projectDataStore.updateData { project.toProto() } @@ -73,8 +74,7 @@ internal class ConfigLocalDataSourceImpl @Inject constructor( override suspend fun getProjectConfiguration(): ProjectConfiguration = configDataStore.data.first().toDomain() - override fun watchProjectConfiguration(): Flow = - configDataStore.data.map(ProtoProjectConfiguration::toDomain) + override fun watchProjectConfiguration(): Flow = configDataStore.data.map(ProtoProjectConfiguration::toDomain) override suspend fun clearProjectConfiguration() { configDataStore.updateData { it.toBuilder().clear().build() } @@ -82,12 +82,12 @@ internal class ConfigLocalDataSourceImpl @Inject constructor( override suspend fun getDeviceConfiguration(): DeviceConfiguration { val config = deviceConfigDataStore.data.first().toDomain() - val tokenizedModules = config.selectedModules.map {moduleId -> - when(moduleId) { + val tokenizedModules = config.selectedModules.map { moduleId -> + when (moduleId) { is TokenizableString.Raw -> tokenizationProcessor.encrypt( decrypted = moduleId, tokenKeyType = TokenKeyType.ModuleId, - project = getProject() + project = getProject(), ) is TokenizableString.Tokenized -> moduleId } @@ -229,6 +229,9 @@ internal class ConfigLocalDataSourceImpl @Inject constructor( moduleOptions = listOf(), maxAge = DEFAULT_DOWN_SYNC_MAX_AGE, ), + samples = SampleSynchronizationConfiguration( + signedUrlBatchSize = 1, + ), ), custom = null, ).toProto() diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSampleUploadMigration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSampleUploadMigration.kt new file mode 100644 index 0000000000..a6eec513e6 --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSampleUploadMigration.kt @@ -0,0 +1,61 @@ +package com.simprints.infra.config.store.local.migrations + +import androidx.datastore.core.DataMigration +import com.simprints.infra.config.store.local.models.ProtoProjectConfiguration +import com.simprints.infra.config.store.local.models.ProtoSampleSynchronizationConfiguration +import com.simprints.infra.logging.LoggingConstants.CrashReportTag.MIGRATION +import com.simprints.infra.logging.Simber +import javax.inject.Inject + +/** + * Can be removed once all the devices have been updated to 2025.3.0 + */ +class ProjectConfigSampleUploadMigration @Inject constructor() : DataMigration { + override suspend fun cleanUp() { + Simber.i("Migration of project configuration sample upload is done", tag = MIGRATION) + } + + override suspend fun shouldMigrate(currentData: ProtoProjectConfiguration) = with(currentData) { + val hasNoSamplesSyncObject = !synchronization.hasSamples() + val eventSyncBatchSizeIsEmpty = synchronization.up.simprints.batchSizes + .let { it.eventUpSyncs == 0 || it.eventDownSyncs == 0 } + + hasNoSamplesSyncObject || eventSyncBatchSizeIsEmpty + } + + override suspend fun migrate(currentData: ProtoProjectConfiguration): ProtoProjectConfiguration { + Simber.i("Start migration of project configuration sample upload", tag = MIGRATION) + + val currentSyncConfig = currentData.synchronization + val currentBatchSizes = currentSyncConfig.up.simprints.batchSizes + val upBatchSizes = currentBatchSizes + .toBuilder() + .setEventUpSyncs(currentBatchSizes.upSyncs) + .setEventDownSyncs(currentBatchSizes.downSyncs) + .setSampleUpSyncs(DEFAULT_BATCH_SIZE) + .build() + val upSyncSimprintsConfig = currentSyncConfig.up.simprints + .toBuilder() + .setBatchSizes(upBatchSizes) + .build() + val upSyncConfig = currentSyncConfig.up + .toBuilder() + .setSimprints(upSyncSimprintsConfig) + .build() + val samplesConfig = ProtoSampleSynchronizationConfiguration.newBuilder().setSignedUrlBatchSize(DEFAULT_BATCH_SIZE).build() + + return currentData + .toBuilder() + .setSynchronization( + currentSyncConfig + .toBuilder() + .setUp(upSyncConfig) + .setSamples(samplesConfig) + .build(), + ).build() + } + + companion object { + private const val DEFAULT_BATCH_SIZE = 1 + } +} diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt index 805b67f9cf..3734a7558c 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt @@ -13,6 +13,7 @@ import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.config.store.models.IdentificationConfiguration import com.simprints.infra.config.store.models.ProjectConfiguration +import com.simprints.infra.config.store.models.SampleSynchronizationConfiguration import com.simprints.infra.config.store.models.SettingsPasswordConfig import com.simprints.infra.config.store.models.SynchronizationConfiguration import com.simprints.infra.config.store.models.UpSynchronizationConfiguration @@ -226,9 +227,10 @@ internal data class OldProjectConfig( ) }, batchSizes = UpSynchronizationConfiguration.UpSyncBatchSizes( - sessions = 1, - upSyncs = 1, - downSyncs = 1, + sessions = DEFAULT_BATCH_SIZE, + eventUpSyncs = DEFAULT_BATCH_SIZE, + eventDownSyncs = DEFAULT_BATCH_SIZE, + sampleUpSyncs = DEFAULT_BATCH_SIZE, ), imagesRequireUnmeteredConnection = false, ), @@ -254,6 +256,9 @@ internal data class OldProjectConfig( moduleOptions = moduleIdOptions.split("|").map(String::asTokenizableRaw), maxAge = DownSynchronizationConfiguration.DEFAULT_DOWN_SYNC_MAX_AGE, ), + samples = SampleSynchronizationConfiguration( + signedUrlBatchSize = DEFAULT_BATCH_SIZE, + ), ) private fun parseDecisionPolicy(decisionPolicy: String): DecisionPolicy = with(JSONObject(decisionPolicy)) { @@ -267,5 +272,6 @@ internal data class OldProjectConfig( companion object { private const val DEFAULT_FACE_FRAMES_TO_CAPTURE = 2 private const val DEFAULT_FACE_SDK_VERSION = "1.23" + private const val DEFAULT_BATCH_SIZE = 1 } } diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/SynchronizationConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/SynchronizationConfiguration.kt index 24e1200afb..a65ee68208 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/SynchronizationConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/SynchronizationConfiguration.kt @@ -1,6 +1,7 @@ package com.simprints.infra.config.store.local.models import com.simprints.infra.config.store.exceptions.InvalidProtobufEnumException +import com.simprints.infra.config.store.local.models.ProtoSynchronizationConfiguration.Frequency import com.simprints.infra.config.store.models.SynchronizationConfiguration internal fun SynchronizationConfiguration.toProto(): ProtoSynchronizationConfiguration = ProtoSynchronizationConfiguration @@ -8,23 +9,25 @@ internal fun SynchronizationConfiguration.toProto(): ProtoSynchronizationConfigu .setFrequency(frequency.toProto()) .setDown(down.toProto()) .setUp(up.toProto()) + .setSamples(samples.toProto()) .build() -internal fun SynchronizationConfiguration.Frequency.toProto(): ProtoSynchronizationConfiguration.Frequency = when (this) { - SynchronizationConfiguration.Frequency.ONLY_PERIODICALLY_UP_SYNC -> ProtoSynchronizationConfiguration.Frequency.ONLY_PERIODICALLY_UP_SYNC - SynchronizationConfiguration.Frequency.PERIODICALLY -> ProtoSynchronizationConfiguration.Frequency.PERIODICALLY - SynchronizationConfiguration.Frequency.PERIODICALLY_AND_ON_SESSION_START -> ProtoSynchronizationConfiguration.Frequency.PERIODICALLY_AND_ON_SESSION_START +internal fun SynchronizationConfiguration.Frequency.toProto(): Frequency = when (this) { + SynchronizationConfiguration.Frequency.ONLY_PERIODICALLY_UP_SYNC -> Frequency.ONLY_PERIODICALLY_UP_SYNC + SynchronizationConfiguration.Frequency.PERIODICALLY -> Frequency.PERIODICALLY + SynchronizationConfiguration.Frequency.PERIODICALLY_AND_ON_SESSION_START -> Frequency.PERIODICALLY_AND_ON_SESSION_START } internal fun ProtoSynchronizationConfiguration.toDomain(): SynchronizationConfiguration = SynchronizationConfiguration( - frequency.toDomain(), - up.toDomain(), - down.toDomain(), + frequency = frequency.toDomain(), + up = up.toDomain(), + down = down.toDomain(), + samples = samples.toDomain(), ) -internal fun ProtoSynchronizationConfiguration.Frequency.toDomain(): SynchronizationConfiguration.Frequency = when (this) { - ProtoSynchronizationConfiguration.Frequency.ONLY_PERIODICALLY_UP_SYNC -> SynchronizationConfiguration.Frequency.ONLY_PERIODICALLY_UP_SYNC - ProtoSynchronizationConfiguration.Frequency.PERIODICALLY -> SynchronizationConfiguration.Frequency.PERIODICALLY - ProtoSynchronizationConfiguration.Frequency.PERIODICALLY_AND_ON_SESSION_START -> SynchronizationConfiguration.Frequency.PERIODICALLY_AND_ON_SESSION_START - ProtoSynchronizationConfiguration.Frequency.UNRECOGNIZED -> throw InvalidProtobufEnumException("invalid Frequency $name") +internal fun Frequency.toDomain(): SynchronizationConfiguration.Frequency = when (this) { + Frequency.ONLY_PERIODICALLY_UP_SYNC -> SynchronizationConfiguration.Frequency.ONLY_PERIODICALLY_UP_SYNC + Frequency.PERIODICALLY -> SynchronizationConfiguration.Frequency.PERIODICALLY + Frequency.PERIODICALLY_AND_ON_SESSION_START -> SynchronizationConfiguration.Frequency.PERIODICALLY_AND_ON_SESSION_START + Frequency.UNRECOGNIZED -> throw InvalidProtobufEnumException("invalid Frequency $name") } diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/UpSynchronizationConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/UpSynchronizationConfiguration.kt index 8c774ed9fb..ffcb4d0ab2 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/UpSynchronizationConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/UpSynchronizationConfiguration.kt @@ -1,7 +1,9 @@ package com.simprints.infra.config.store.local.models import com.simprints.infra.config.store.exceptions.InvalidProtobufEnumException +import com.simprints.infra.config.store.models.SampleSynchronizationConfiguration import com.simprints.infra.config.store.models.UpSynchronizationConfiguration +import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.UpSynchronizationKind internal fun UpSynchronizationConfiguration.toProto(): ProtoUpSynchronizationConfiguration = ProtoUpSynchronizationConfiguration .newBuilder() @@ -9,7 +11,7 @@ internal fun UpSynchronizationConfiguration.toProto(): ProtoUpSynchronizationCon .setCoSync(coSync.toProto()) .build() -internal fun UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration.toProto(): ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration = +internal fun UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration.toProto() = ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration .newBuilder() .setKind(kind.toProto()) @@ -17,50 +19,60 @@ internal fun UpSynchronizationConfiguration.SimprintsUpSynchronizationConfigurat .setImagesRequireUnmeteredConnection(imagesRequireUnmeteredConnection) .build() -internal fun UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration.toProto(): ProtoUpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration = +internal fun UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration.toProto() = ProtoUpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration .newBuilder() .setKind(kind.toProto()) .build() -internal fun UpSynchronizationConfiguration.UpSynchronizationKind.toProto(): ProtoUpSynchronizationConfiguration.UpSynchronizationKind = - when (this) { - UpSynchronizationConfiguration.UpSynchronizationKind.NONE -> ProtoUpSynchronizationConfiguration.UpSynchronizationKind.NONE - UpSynchronizationConfiguration.UpSynchronizationKind.ONLY_ANALYTICS -> ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ONLY_ANALYTICS - UpSynchronizationConfiguration.UpSynchronizationKind.ONLY_BIOMETRICS -> ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ONLY_BIOMETRICS - UpSynchronizationConfiguration.UpSynchronizationKind.ALL -> ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ALL - } +internal fun UpSynchronizationKind.toProto(): ProtoUpSynchronizationConfiguration.UpSynchronizationKind = when (this) { + UpSynchronizationKind.NONE -> ProtoUpSynchronizationConfiguration.UpSynchronizationKind.NONE + UpSynchronizationKind.ONLY_ANALYTICS -> ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ONLY_ANALYTICS + UpSynchronizationKind.ONLY_BIOMETRICS -> ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ONLY_BIOMETRICS + UpSynchronizationKind.ALL -> ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ALL +} -internal fun UpSynchronizationConfiguration.UpSyncBatchSizes.toProto(): ProtoUpSyncBatchSizes = ProtoUpSyncBatchSizes +internal fun UpSynchronizationConfiguration.UpSyncBatchSizes.toProto() = ProtoUpSyncBatchSizes .newBuilder() .setSessions(sessions) - .setUpSyncs(upSyncs) - .setDownSyncs(downSyncs) + .setEventUpSyncs(eventUpSyncs) + .setEventDownSyncs(eventDownSyncs) + .setSampleUpSyncs(sampleUpSyncs) .build() -internal fun ProtoUpSynchronizationConfiguration.toDomain(): UpSynchronizationConfiguration = - UpSynchronizationConfiguration(simprints.toDomain(), coSync.toDomain()) +internal fun SampleSynchronizationConfiguration.toProto() = ProtoSampleSynchronizationConfiguration + .newBuilder() + .setSignedUrlBatchSize(signedUrlBatchSize) + .build() + +internal fun ProtoUpSynchronizationConfiguration.toDomain() = UpSynchronizationConfiguration( + simprints.toDomain(), + coSync.toDomain(), +) -internal fun ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration.toDomain(): UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration = +internal fun ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration.toDomain() = UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration( kind.toDomain(), batchSizes.toDomain(), imagesRequireUnmeteredConnection, ) -internal fun ProtoUpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration.toDomain(): UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration = +internal fun ProtoUpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration.toDomain() = UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration(kind.toDomain()) -internal fun ProtoUpSynchronizationConfiguration.UpSynchronizationKind.toDomain(): UpSynchronizationConfiguration.UpSynchronizationKind = - when (this) { - ProtoUpSynchronizationConfiguration.UpSynchronizationKind.NONE -> UpSynchronizationConfiguration.UpSynchronizationKind.NONE - ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ONLY_ANALYTICS -> UpSynchronizationConfiguration.UpSynchronizationKind.ONLY_ANALYTICS - ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ONLY_BIOMETRICS -> UpSynchronizationConfiguration.UpSynchronizationKind.ONLY_BIOMETRICS - ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ALL -> UpSynchronizationConfiguration.UpSynchronizationKind.ALL - ProtoUpSynchronizationConfiguration.UpSynchronizationKind.UNRECOGNIZED -> throw InvalidProtobufEnumException( - "invalid UpSynchronizationKind $name", - ) - } +internal fun ProtoUpSynchronizationConfiguration.UpSynchronizationKind.toDomain() = when (this) { + ProtoUpSynchronizationConfiguration.UpSynchronizationKind.NONE -> UpSynchronizationKind.NONE + ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ONLY_ANALYTICS -> UpSynchronizationKind.ONLY_ANALYTICS + ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ONLY_BIOMETRICS -> UpSynchronizationKind.ONLY_BIOMETRICS + ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ALL -> UpSynchronizationKind.ALL + ProtoUpSynchronizationConfiguration.UpSynchronizationKind.UNRECOGNIZED -> throw InvalidProtobufEnumException( + "invalid UpSynchronizationKind $name", + ) +} internal fun ProtoUpSyncBatchSizes.toDomain(): UpSynchronizationConfiguration.UpSyncBatchSizes = - UpSynchronizationConfiguration.UpSyncBatchSizes(sessions, upSyncs, downSyncs) + UpSynchronizationConfiguration.UpSyncBatchSizes(sessions, eventUpSyncs, eventDownSyncs, sampleUpSyncs) + +internal fun ProtoSampleSynchronizationConfiguration.toDomain() = SampleSynchronizationConfiguration( + signedUrlBatchSize = signedUrlBatchSize, +) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt index d6f058a7e8..d0d2e00aaa 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt @@ -44,17 +44,25 @@ data class ExperimentalProjectConfiguration( (it as? Int) ?: RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_DEFAULT_MAX_RETRIES } + val sampleUploadWithSignedUrlEnabled: Boolean + get() = customConfig + ?.get(SAMPLE_UPLOAD_WITH_URL_ENABLED) + ?.let { it as? Boolean } + .let { it == true } + companion object { internal const val ENABLE_ID_POOL_VALIDATION = "validateIdentificationPool" internal const val SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED = "singleQualityFallbackRequired" internal const val FACE_AUTO_CAPTURE_ENABLED = "faceAutoCaptureEnabled" internal const val FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS = "faceAutoCaptureImagingDurationMillis" - const val RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED = "recordsDbMigrationFromRealmEnabled" + internal const val RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED = "recordsDbMigrationFromRealmEnabled" const val RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES = "recordsDbMigrationFromRealmMaxRetries" - const val RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_DEFAULT_MAX_RETRIES = 10 - const val FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_MIN = 1L + internal const val RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_DEFAULT_MAX_RETRIES = 10 + internal const val FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_MIN = 1L const val FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT = 3_000L - const val FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_MAX = 60_000L + internal const val FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_MAX = 60_000L + + internal const val SAMPLE_UPLOAD_WITH_URL_ENABLED = "sampleUploadWithSignedUrl" } } diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/SampleSynchronizationConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/SampleSynchronizationConfiguration.kt new file mode 100644 index 0000000000..699fa66481 --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/SampleSynchronizationConfiguration.kt @@ -0,0 +1,5 @@ +package com.simprints.infra.config.store.models + +data class SampleSynchronizationConfiguration( + val signedUrlBatchSize: Int, +) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/SynchronizationConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/SynchronizationConfiguration.kt index 632e0639b6..f1b7787954 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/SynchronizationConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/SynchronizationConfiguration.kt @@ -4,6 +4,7 @@ data class SynchronizationConfiguration( val frequency: Frequency, val up: UpSynchronizationConfiguration, val down: DownSynchronizationConfiguration, + val samples: SampleSynchronizationConfiguration, ) { enum class Frequency { ONLY_PERIODICALLY_UP_SYNC, diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/UpSynchronizationConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/UpSynchronizationConfiguration.kt index 6927681952..3abd619e10 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/UpSynchronizationConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/UpSynchronizationConfiguration.kt @@ -16,11 +16,12 @@ data class UpSynchronizationConfiguration( data class UpSyncBatchSizes( val sessions: Int, - val upSyncs: Int, - val downSyncs: Int, + val eventUpSyncs: Int, + val eventDownSyncs: Int, + val sampleUpSyncs: Int, ) { companion object { - fun default() = UpSyncBatchSizes(1, 1, 1) + fun default() = UpSyncBatchSizes(sessions = 1, eventUpSyncs = 1, eventDownSyncs = 1, sampleUpSyncs = 1) } } diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSynchronizationConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSynchronizationConfiguration.kt index 3841ddb498..f7be642a23 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSynchronizationConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSynchronizationConfiguration.kt @@ -4,6 +4,7 @@ import androidx.annotation.Keep import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.config.store.models.DownSynchronizationConfiguration import com.simprints.infra.config.store.models.DownSynchronizationConfiguration.Companion.DEFAULT_DOWN_SYNC_MAX_AGE +import com.simprints.infra.config.store.models.SampleSynchronizationConfiguration import com.simprints.infra.config.store.models.SynchronizationConfiguration import com.simprints.infra.config.store.models.UpSynchronizationConfiguration @@ -12,11 +13,13 @@ internal data class ApiSynchronizationConfiguration( val frequency: Frequency, val up: ApiUpSynchronizationConfiguration, val down: ApiDownSynchronizationConfiguration, + val sample: ApiSampleSynchronizationConfiguration, ) { fun toDomain(): SynchronizationConfiguration = SynchronizationConfiguration( - frequency.toDomain(), - up.toDomain(), - down.toDomain(), + frequency = frequency.toDomain(), + up = up.toDomain(), + down = down.toDomain(), + samples = sample.toDomain(), ) @Keep @@ -85,11 +88,12 @@ internal data class ApiSynchronizationConfiguration( @Keep data class ApiUpSyncBatchSizes( val sessions: Int, - val upSyncs: Int, - val downSyncs: Int, + val eventUpSyncs: Int, + val eventDownSyncs: Int, + val sampleUpSyncs: Int, ) { fun toDomain(): UpSynchronizationConfiguration.UpSyncBatchSizes = - UpSynchronizationConfiguration.UpSyncBatchSizes(sessions, upSyncs, downSyncs) + UpSynchronizationConfiguration.UpSyncBatchSizes(sessions, eventUpSyncs, eventDownSyncs, sampleUpSyncs) } } @@ -121,4 +125,13 @@ internal data class ApiSynchronizationConfiguration( } } } + + @Keep + data class ApiSampleSynchronizationConfiguration( + val signedUrlBatchSize: Int, + ) { + fun toDomain() = SampleSynchronizationConfiguration( + signedUrlBatchSize = signedUrlBatchSize, + ) + } } diff --git a/infra/config-store/src/main/proto/project_config.proto b/infra/config-store/src/main/proto/project_config.proto index b8227fef36..ac0c1faa4d 100644 --- a/infra/config-store/src/main/proto/project_config.proto +++ b/infra/config-store/src/main/proto/project_config.proto @@ -192,6 +192,7 @@ message ProtoSynchronizationConfiguration { Frequency frequency = 1; ProtoUpSynchronizationConfiguration up = 2; ProtoDownSynchronizationConfiguration down = 3; + ProtoSampleSynchronizationConfiguration samples = 4; enum Frequency { ONLY_PERIODICALLY_UP_SYNC = 0; @@ -236,10 +237,17 @@ message ProtoUpSynchronizationConfiguration { } } +message ProtoSampleSynchronizationConfiguration { + int32 signedUrlBatchSize = 1; +} + message ProtoUpSyncBatchSizes { int32 sessions = 1; - int32 upSyncs = 2; - int32 downSyncs = 3; + int32 upSyncs = 2; // TODO: remove this field after migration to 2025.3.0 + int32 downSyncs = 3; // TODO: remove this field after migration to 2025.3.0 + int32 eventUpSyncs = 4; + int32 eventDownSyncs = 5; + int32 sampleUpSyncs = 6; } message ProtoDecisionPolicy { diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSampleUploadMigrationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSampleUploadMigrationTest.kt new file mode 100644 index 0000000000..c07baf78fe --- /dev/null +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSampleUploadMigrationTest.kt @@ -0,0 +1,115 @@ +package com.simprints.infra.config.store.local.migrations + +import com.google.common.truth.Truth.assertThat +import com.simprints.infra.config.store.local.models.ProtoProjectConfiguration +import com.simprints.infra.config.store.local.models.ProtoSampleSynchronizationConfiguration +import com.simprints.infra.config.store.local.models.ProtoSynchronizationConfiguration +import com.simprints.infra.config.store.local.models.ProtoUpSyncBatchSizes +import com.simprints.infra.config.store.local.models.ProtoUpSynchronizationConfiguration +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ProjectConfigSampleUploadMigrationTest { + @Test + fun `should not migrate if sync config has samples object`() = runTest { + val currentData = ProtoProjectConfiguration + .newBuilder() + .setSynchronization( + ProtoSynchronizationConfiguration + .newBuilder() + .setSamples(ProtoSampleSynchronizationConfiguration.newBuilder().build()) + .setUp( + ProtoUpSynchronizationConfiguration + .newBuilder() + .setSimprints( + ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration + .newBuilder() + .setBatchSizes( + ProtoUpSyncBatchSizes + .newBuilder() + .setEventUpSyncs(1) + .setEventDownSyncs(1) + .build(), + ).build(), + ).build(), + ).build(), + ).build() + assertThat(ProjectConfigSampleUploadMigration().shouldMigrate(currentData)).isFalse() + } + + @Test + fun `should migrate if sync config has no samples config`() = runTest { + val currentData = ProtoProjectConfiguration + .newBuilder() + .setSynchronization( + ProtoSynchronizationConfiguration + .newBuilder() + .setSamples(ProtoSampleSynchronizationConfiguration.newBuilder().build()) + .setUp( + ProtoUpSynchronizationConfiguration + .newBuilder() + .setSimprints( + ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration + .newBuilder() + .setBatchSizes( + ProtoUpSyncBatchSizes + .newBuilder() + .setUpSyncs(1) + .setDownSyncs(1) + .build(), + ).build(), + ).build(), + ).build(), + ).build() + + assertThat(ProjectConfigSampleUploadMigration().shouldMigrate(currentData)).isTrue() + } + + @Test + fun `should migrate if sync config has no events upsync config`() = runTest { + val currentData = ProtoProjectConfiguration + .newBuilder() + .setSynchronization( + ProtoSynchronizationConfiguration + .newBuilder() + .clearSamples() + .build(), + ).build() + + assertThat(ProjectConfigSampleUploadMigration().shouldMigrate(currentData)).isTrue() + } + + @Test + fun `should add default values for samples config`() = runTest { + val currentData = ProtoProjectConfiguration + .newBuilder() + .setSynchronization( + ProtoSynchronizationConfiguration + .newBuilder() + .clearSamples() + .setUp( + ProtoUpSynchronizationConfiguration + .newBuilder() + .setSimprints( + ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration + .newBuilder() + .setBatchSizes( + ProtoUpSyncBatchSizes + .newBuilder() + .setUpSyncs(1) + .setDownSyncs(1) + .build(), + ).build(), + ).build(), + ).build(), + ).build() + + val result = ProjectConfigSampleUploadMigration().migrate(currentData) + + assertThat(result.synchronization.hasSamples()).isTrue() + assertThat(result.synchronization.samples?.signedUrlBatchSize).isEqualTo(1) + assertThat(result.synchronization.up.simprints.batchSizes.sampleUpSyncs).isEqualTo(1) + assertThat(result.synchronization.up.simprints.batchSizes.eventUpSyncs).isEqualTo(1) + assertThat(result.synchronization.up.simprints.batchSizes.eventDownSyncs).isEqualTo(1) + } +} diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt index 0530a8e341..fcf3aaea60 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt @@ -19,6 +19,7 @@ import com.simprints.infra.config.store.local.models.ProtoFingerprintConfigurati import com.simprints.infra.config.store.local.models.ProtoGeneralConfiguration import com.simprints.infra.config.store.local.models.ProtoIdentificationConfiguration import com.simprints.infra.config.store.local.models.ProtoProjectConfiguration +import com.simprints.infra.config.store.local.models.ProtoSampleSynchronizationConfiguration import com.simprints.infra.config.store.local.models.ProtoSynchronizationConfiguration import com.simprints.infra.config.store.local.models.ProtoUpSyncBatchSizes import com.simprints.infra.config.store.local.models.ProtoUpSynchronizationConfiguration @@ -537,8 +538,9 @@ class ProjectConfigSharedPrefsMigrationTest { ProtoUpSyncBatchSizes .newBuilder() .setSessions(1) - .setUpSyncs(1) - .setDownSyncs(1) + .setEventUpSyncs(1) + .setEventDownSyncs(1) + .setSampleUpSyncs(1) .build(), ).build(), ).setCoSync( @@ -555,6 +557,8 @@ class ProjectConfigSharedPrefsMigrationTest { .addAllModuleOptions(listOf("module1", "module2")) .setMaxAge("PT24H") .build(), + ).setSamples( + ProtoSampleSynchronizationConfiguration.newBuilder().setSignedUrlBatchSize(1).build(), ).build() private val PROTO_SYNCHRONIZATION_CONFIGURATION_NON_NULL_VALUES = @@ -572,8 +576,9 @@ class ProjectConfigSharedPrefsMigrationTest { ProtoUpSyncBatchSizes .newBuilder() .setSessions(1) - .setUpSyncs(1) - .setDownSyncs(1) + .setEventUpSyncs(1) + .setEventDownSyncs(1) + .setSampleUpSyncs(1) .build(), ).build(), ).setCoSync( @@ -590,6 +595,8 @@ class ProjectConfigSharedPrefsMigrationTest { .addAllModuleOptions(listOf("module1", "module2")) .setMaxAge("PT24H") .build(), + ).setSamples( + ProtoSampleSynchronizationConfiguration.newBuilder().setSignedUrlBatchSize(1).build(), ).build() private val PROTO_SYNCHRONIZATION_CONFIGURATION_EMPTY_VALUES = @@ -607,8 +614,9 @@ class ProjectConfigSharedPrefsMigrationTest { ProtoUpSyncBatchSizes .newBuilder() .setSessions(1) - .setUpSyncs(1) - .setDownSyncs(1) + .setEventUpSyncs(1) + .setEventDownSyncs(1) + .setSampleUpSyncs(1) .build(), ).build(), ).setCoSync( @@ -625,6 +633,8 @@ class ProjectConfigSharedPrefsMigrationTest { .addAllModuleOptions(listOf("module1", "module2")) .setMaxAge("PT24H") .build(), + ).setSamples( + ProtoSampleSynchronizationConfiguration.newBuilder().setSignedUrlBatchSize(1).build(), ).build() private val PROTO_SYNCHRONIZATION_CONFIGURATION_NULL_VALUES = @@ -642,8 +652,9 @@ class ProjectConfigSharedPrefsMigrationTest { ProtoUpSyncBatchSizes .newBuilder() .setSessions(1) - .setUpSyncs(1) - .setDownSyncs(1) + .setEventUpSyncs(1) + .setEventDownSyncs(1) + .setSampleUpSyncs(1) .build(), ).build(), ).setCoSync( @@ -660,6 +671,8 @@ class ProjectConfigSharedPrefsMigrationTest { .addAllModuleOptions(listOf("module1", "module2")) .setMaxAge("PT24H") .build(), + ).setSamples( + ProtoSampleSynchronizationConfiguration.newBuilder().setSignedUrlBatchSize(1).build(), ).build() private val JSON_IDENTIFICATION_CONFIGURATION = diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt index 59b18be0ba..407fedab14 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt @@ -1,19 +1,23 @@ package com.simprints.infra.config.store.models -import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.* import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.ENABLE_ID_POOL_VALIDATION -import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.FACE_AUTO_CAPTURE_ENABLED import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_MAX import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_MIN +import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_DEFAULT_MAX_RETRIES +import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED +import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES +import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.SAMPLE_UPLOAD_WITH_URL_ENABLED +import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED import org.junit.Test internal class ExperimentalProjectConfigurationTest { @Test fun `check pool validation flag correctly`() { - mapOf, Boolean>( + mapOf( // Value not present emptyMap() to false, // Value not boolean @@ -29,7 +33,7 @@ internal class ExperimentalProjectConfigurationTest { @Test fun `check single good face capture fallback flag correctly`() { - mapOf, Boolean>( + mapOf( // Value not present emptyMap() to false, // Value not boolean @@ -45,7 +49,7 @@ internal class ExperimentalProjectConfigurationTest { @Test fun `check face auto capture flag correctly`() { - mapOf, Boolean>( + mapOf( // Value not present emptyMap() to false, // Value not boolean @@ -61,7 +65,7 @@ internal class ExperimentalProjectConfigurationTest { @Test fun `check face auto capture imaging duration flag correctly`() { - mapOf, Long>( + mapOf( // Value not present emptyMap() to FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT, // Value not int @@ -76,4 +80,51 @@ internal class ExperimentalProjectConfigurationTest { assertThat(ExperimentalProjectConfiguration(config).faceAutoCaptureImagingDurationMillis).isEqualTo(result) } } + + @Test + fun `check records DB migration flag correctly`() { + mapOf( + // Value not present + emptyMap() to false, + // Value not boolean + mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED to 1) to false, + // Value present and FALSE + mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED to false) to false, + // Value present and TRUE + mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED to true) to true, + ).forEach { (config, result) -> + assertThat(ExperimentalProjectConfiguration(config).recordsDbMigrationFromRealmEnabled).isEqualTo(result) + } + } + + @Test + fun `check records DB migration max retries parsed correctly`() { + mapOf( + // Value not present + emptyMap() to RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_DEFAULT_MAX_RETRIES, + // Value not int + mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES to true) to + RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_DEFAULT_MAX_RETRIES, + // Value is int + mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES to 3) to 3, + ).forEach { (config, result) -> + assertThat(ExperimentalProjectConfiguration(config).recordsDbMigrationFromRealmMaxRetries).isEqualTo(result) + } + } + + @Test + fun `check signed url enabled flag correctly`() { + mapOf( + // Value not present + emptyMap() to false, + // Value not boolean + mapOf(SAMPLE_UPLOAD_WITH_URL_ENABLED to 1) to false, + // Value present and FALSE + mapOf(SAMPLE_UPLOAD_WITH_URL_ENABLED to false) to false, + // Value present and TRUE + mapOf(SAMPLE_UPLOAD_WITH_URL_ENABLED to true) to true, + ).forEach { (config, result) -> + assertThat(ExperimentalProjectConfiguration(config).sampleUploadWithSignedUrlEnabled).isEqualTo(result) + } + } } diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt index 8289ad55e6..b0d38aa809 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt @@ -14,6 +14,7 @@ import com.simprints.infra.config.store.local.models.ProtoGeneralConfiguration import com.simprints.infra.config.store.local.models.ProtoIdentificationConfiguration import com.simprints.infra.config.store.local.models.ProtoProject import com.simprints.infra.config.store.local.models.ProtoProjectConfiguration +import com.simprints.infra.config.store.local.models.ProtoSampleSynchronizationConfiguration import com.simprints.infra.config.store.local.models.ProtoSynchronizationConfiguration import com.simprints.infra.config.store.local.models.ProtoUpSyncBatchSizes import com.simprints.infra.config.store.local.models.ProtoUpSynchronizationConfiguration @@ -35,6 +36,7 @@ import com.simprints.infra.config.store.models.MaxCaptureAttempts import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.ProjectState +import com.simprints.infra.config.store.models.SampleSynchronizationConfiguration import com.simprints.infra.config.store.models.SettingsPasswordConfig import com.simprints.infra.config.store.models.SynchronizationConfiguration import com.simprints.infra.config.store.models.TokenKeyType @@ -336,44 +338,60 @@ internal val protoIdentificationConfiguration = ProtoIdentificationConfiguration .build() internal val apiSynchronizationConfiguration = ApiSynchronizationConfiguration( - ApiSynchronizationConfiguration.Frequency.PERIODICALLY, - ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration( - ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.ApiSimprintsUpSynchronizationConfiguration( - ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.UpSynchronizationKind.ALL, - ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.ApiUpSyncBatchSizes(1, 2, 3), - false, + frequency = ApiSynchronizationConfiguration.Frequency.PERIODICALLY, + up = ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration( + simprints = ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.ApiSimprintsUpSynchronizationConfiguration( + kind = ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.UpSynchronizationKind.ALL, + batchSizes = ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.ApiUpSyncBatchSizes( + sessions = 1, + eventUpSyncs = 2, + eventDownSyncs = 3, + sampleUpSyncs = 4, + ), + imagesRequireUnmeteredConnection = false, ), - ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.ApiCoSyncUpSynchronizationConfiguration( - ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.UpSynchronizationKind.NONE, + coSync = ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.ApiCoSyncUpSynchronizationConfiguration( + kind = ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.UpSynchronizationKind.NONE, ), ), - ApiSynchronizationConfiguration.ApiDownSynchronizationConfiguration( - ApiSynchronizationConfiguration.ApiDownSynchronizationConfiguration.PartitionType.PROJECT, - 1, - listOf("module1"), - "PT24H", + down = ApiSynchronizationConfiguration.ApiDownSynchronizationConfiguration( + partitionType = ApiSynchronizationConfiguration.ApiDownSynchronizationConfiguration.PartitionType.PROJECT, + maxNbOfModules = 1, + moduleOptions = listOf("module1"), + maxAge = "PT24H", + ), + sample = ApiSynchronizationConfiguration.ApiSampleSynchronizationConfiguration( + signedUrlBatchSize = 5, ), ) internal val simprintsUpSyncConfigurationConfiguration = UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration( - UpSynchronizationConfiguration.UpSynchronizationKind.ALL, - UpSynchronizationConfiguration.UpSyncBatchSizes(1, 2, 3), - false, + kind = UpSynchronizationConfiguration.UpSynchronizationKind.ALL, + batchSizes = UpSynchronizationConfiguration.UpSyncBatchSizes( + sessions = 1, + eventUpSyncs = 2, + eventDownSyncs = 3, + sampleUpSyncs = 4, + ), + imagesRequireUnmeteredConnection = false, ) internal val synchronizationConfiguration = SynchronizationConfiguration( - SynchronizationConfiguration.Frequency.PERIODICALLY, - UpSynchronizationConfiguration( - simprintsUpSyncConfigurationConfiguration, - UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration( + frequency = SynchronizationConfiguration.Frequency.PERIODICALLY, + up = UpSynchronizationConfiguration( + simprints = simprintsUpSyncConfigurationConfiguration, + coSync = UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration( UpSynchronizationConfiguration.UpSynchronizationKind.NONE, ), ), - DownSynchronizationConfiguration( - DownSynchronizationConfiguration.PartitionType.PROJECT, - 1, - listOf("module1".asTokenizableEncrypted()), - "PT24H", + down = DownSynchronizationConfiguration( + partitionType = DownSynchronizationConfiguration.PartitionType.PROJECT, + maxNbOfModules = 1, + moduleOptions = listOf("module1".asTokenizableEncrypted()), + maxAge = "PT24H", + ), + samples = SampleSynchronizationConfiguration( + signedUrlBatchSize = 5, ), ) @@ -391,8 +409,9 @@ internal val protoSynchronizationConfiguration = ProtoSynchronizationConfigurati ProtoUpSyncBatchSizes .newBuilder() .setSessions(1) - .setUpSyncs(2) - .setDownSyncs(3) + .setEventUpSyncs(2) + .setEventDownSyncs(3) + .setSampleUpSyncs(4) .build(), ).build(), ).setCoSync( @@ -410,6 +429,11 @@ internal val protoSynchronizationConfiguration = ProtoSynchronizationConfigurati .addModuleOptions("module1") .setMaxAge("PT24H") .build(), + ).setSamples( + ProtoSampleSynchronizationConfiguration + .newBuilder() + .setSignedUrlBatchSize(5) + .build(), ).build() internal val customKeyMap: Map? = mapOf( diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/ApiUploadEventsBody.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/ApiUploadEventsBody.kt index 7c94224fc9..76106109e3 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/ApiUploadEventsBody.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/ApiUploadEventsBody.kt @@ -8,4 +8,5 @@ internal data class ApiUploadEventsBody( val sessions: List = emptyList(), val eventUpSyncs: List = emptyList(), val eventDownSyncs: List = emptyList(), + val sampleUpSyncs: List = emptyList(), ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt index f5c37dd2c5..0bf9a3677c 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt @@ -58,6 +58,7 @@ import com.simprints.infra.events.event.domain.models.EventType.ONE_TO_MANY_MATC import com.simprints.infra.events.event.domain.models.EventType.ONE_TO_ONE_MATCH import com.simprints.infra.events.event.domain.models.EventType.PERSON_CREATION import com.simprints.infra.events.event.domain.models.EventType.REFUSAL +import com.simprints.infra.events.event.domain.models.EventType.SAMPLE_UP_SYNC_REQUEST import com.simprints.infra.events.event.domain.models.EventType.SCANNER_CONNECTION import com.simprints.infra.events.event.domain.models.EventType.SCANNER_FIRMWARE_UPDATE import com.simprints.infra.events.event.domain.models.EventType.SUSPICIOUS_INTENT @@ -98,6 +99,7 @@ import com.simprints.infra.events.event.domain.models.face.FaceFallbackCaptureEv import com.simprints.infra.events.event.domain.models.face.FaceOnboardingCompleteEvent.FaceOnboardingCompletePayload import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureBiometricsEvent.FingerprintCaptureBiometricsPayload import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent +import com.simprints.infra.events.event.domain.models.samples.SampleUpSyncRequestEvent import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestEvent import com.simprints.infra.eventsync.event.remote.models.callback.ApiCallbackPayload import com.simprints.infra.eventsync.event.remote.models.callout.ApiCalloutPayloadV2 @@ -108,6 +110,7 @@ import com.simprints.infra.eventsync.event.remote.models.face.ApiFaceCaptureConf import com.simprints.infra.eventsync.event.remote.models.face.ApiFaceCapturePayload import com.simprints.infra.eventsync.event.remote.models.face.ApiFaceFallbackCapturePayload import com.simprints.infra.eventsync.event.remote.models.face.ApiFaceOnboardingCompletePayload +import com.simprints.infra.eventsync.event.remote.models.samples.ApiEventSampleUpSyncRequestPayload import com.simprints.infra.eventsync.event.remote.models.upsync.ApiEventUpSyncRequestPayload @Keep @@ -163,6 +166,7 @@ internal fun EventPayload.fromDomainToApi(): ApiEventPayload = when (this.type) FACE_CAPTURE_BIOMETRICS -> ApiFaceCaptureBiometricsPayload(this as FaceCaptureBiometricsEvent.FaceCaptureBiometricsPayload) EVENT_DOWN_SYNC_REQUEST -> ApiEventDownSyncRequestPayload(this as EventDownSyncRequestEvent.EventDownSyncRequestPayload) EVENT_UP_SYNC_REQUEST -> ApiEventUpSyncRequestPayload(this as EventUpSyncRequestEvent.EventUpSyncRequestPayload) + SAMPLE_UP_SYNC_REQUEST -> ApiEventSampleUpSyncRequestPayload(this as SampleUpSyncRequestEvent.SampleUpSyncRequestPayload) LICENSE_CHECK -> ApiLicenseCheckEventPayload(this as LicenseCheckEvent.LicenseCheckEventPayload) AGE_GROUP_SELECTION -> ApiAgeGroupSelectionPayload(this as AgeGroupSelectionEvent.AgeGroupSelectionPayload) BIOMETRIC_REFERENCE_CREATION -> ApiBiometricReferenceCreationPayload(this as BiometricReferenceCreationPayload) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt index b52998bb12..e2a4908b20 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt @@ -46,6 +46,7 @@ import com.simprints.infra.events.event.domain.models.EventType.ONE_TO_MANY_MATC import com.simprints.infra.events.event.domain.models.EventType.ONE_TO_ONE_MATCH import com.simprints.infra.events.event.domain.models.EventType.PERSON_CREATION import com.simprints.infra.events.event.domain.models.EventType.REFUSAL +import com.simprints.infra.events.event.domain.models.EventType.SAMPLE_UP_SYNC_REQUEST import com.simprints.infra.events.event.domain.models.EventType.SCANNER_CONNECTION import com.simprints.infra.events.event.domain.models.EventType.SCANNER_FIRMWARE_UPDATE import com.simprints.infra.events.event.domain.models.EventType.SUSPICIOUS_INTENT @@ -83,10 +84,10 @@ internal enum class ApiEventPayloadType { FaceCaptureConfirmation, EventDownSyncRequest, EventUpSyncRequest, + SampleUpSyncRequest, LicenseCheck, AgeGroupSelection, BiometricReferenceCreation, - ; } internal fun EventType.fromDomainToApi(): ApiEventPayloadType = when (this) { @@ -138,6 +139,7 @@ internal fun EventType.fromDomainToApi(): ApiEventPayloadType = when (this) { FACE_CAPTURE_BIOMETRICS -> ApiEventPayloadType.FaceCaptureBiometrics EVENT_DOWN_SYNC_REQUEST -> ApiEventPayloadType.EventDownSyncRequest EVENT_UP_SYNC_REQUEST -> ApiEventPayloadType.EventUpSyncRequest + SAMPLE_UP_SYNC_REQUEST -> ApiEventPayloadType.SampleUpSyncRequest LICENSE_CHECK -> ApiEventPayloadType.LicenseCheck AGE_GROUP_SELECTION -> ApiEventPayloadType.AgeGroupSelection BIOMETRIC_REFERENCE_CREATION -> ApiEventPayloadType.BiometricReferenceCreation @@ -172,6 +174,7 @@ internal fun ApiEventPayloadType.fromApiToDomain(): EventType = when (this) { ApiEventPayloadType.FaceCaptureBiometrics -> FACE_CAPTURE_BIOMETRICS ApiEventPayloadType.EventDownSyncRequest -> EVENT_DOWN_SYNC_REQUEST ApiEventPayloadType.EventUpSyncRequest -> EVENT_UP_SYNC_REQUEST + ApiEventPayloadType.SampleUpSyncRequest -> SAMPLE_UP_SYNC_REQUEST ApiEventPayloadType.LicenseCheck -> LICENSE_CHECK ApiEventPayloadType.AgeGroupSelection -> AGE_GROUP_SELECTION ApiEventPayloadType.BiometricReferenceCreation -> BIOMETRIC_REFERENCE_CREATION diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/samples/ApiEventSampleUpSyncRequestPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/samples/ApiEventSampleUpSyncRequestPayload.kt new file mode 100644 index 0000000000..46474be1eb --- /dev/null +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/samples/ApiEventSampleUpSyncRequestPayload.kt @@ -0,0 +1,29 @@ +package com.simprints.infra.eventsync.event.remote.models.samples + +import androidx.annotation.Keep +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.samples.SampleUpSyncRequestEvent +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayload +import com.simprints.infra.eventsync.event.remote.models.ApiTimestamp +import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi + +@Keep +internal data class ApiEventSampleUpSyncRequestPayload( + override val startTime: ApiTimestamp, + val endTime: ApiTimestamp?, + val requestId: String, + val sampleId: String, + val size: Int, + val errorType: String?, +) : ApiEventPayload(startTime) { + constructor(domainPayload: SampleUpSyncRequestEvent.SampleUpSyncRequestPayload) : this( + domainPayload.createdAt.fromDomainToApi(), + domainPayload.endedAt?.fromDomainToApi(), + domainPayload.requestId, + domainPayload.sampleId, + domainPayload.size, + domainPayload.errorType, + ) + + override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = null +} diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/upsync/ApiEventUpSyncRequestPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/upsync/ApiEventUpSyncRequestPayload.kt index 779a55f45e..6879a91c43 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/upsync/ApiEventUpSyncRequestPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/upsync/ApiEventUpSyncRequestPayload.kt @@ -24,6 +24,7 @@ internal data class ApiEventUpSyncRequestPayload( domainPayload.content.sessionCount, domainPayload.content.eventUpSyncCount, domainPayload.content.eventDownSyncCount, + domainPayload.content.sampleUpSyncCount, ), domainPayload.responseStatus, domainPayload.errorType, @@ -34,6 +35,7 @@ internal data class ApiEventUpSyncRequestPayload( val sessionCount: Int, val eventUpSyncCount: Int, val eventDownSyncCount: Int, + val sampleUpSyncCount: Int, ) override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = null diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt index d9f4013e11..58efb54f81 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt @@ -90,7 +90,7 @@ internal class EventUpSyncTask @Inject constructor( events?.let { filterEventsToUpSync(it, config) } } }, - createUpSyncContentContent = { + createUpSyncContent = { isUsefulUpload = it > 0 EventUpSyncRequestEvent.UpSyncContent(sessionCount = it) }, @@ -101,13 +101,29 @@ internal class EventUpSyncTask @Inject constructor( ) emitProgress(lastOperation, count) } + uploadEventScopeType( + eventScope = eventScope, + project = project, + eventScopeTypeToUpload = EventScopeType.SAMPLE_UP_SYNC, + batchSize = config.synchronization.up.simprints.batchSizes.sampleUpSyncs, + createUpSyncContent = { + isUsefulUpload = isUsefulUpload || it > 0 + EventUpSyncRequestEvent.UpSyncContent(sampleUpSyncCount = it) + }, + ).collect { count -> + lastOperation = lastOperation.copy( + lastState = RUNNING, + lastSyncTime = timeHelper.now().ms, + ) + emitProgress(lastOperation, count) + } uploadEventScopeType( eventScope = eventScope, project = project, eventScopeTypeToUpload = EventScopeType.DOWN_SYNC, - batchSize = config.synchronization.up.simprints.batchSizes.downSyncs, - createUpSyncContentContent = { - isUsefulUpload = it > 0 + batchSize = config.synchronization.up.simprints.batchSizes.eventDownSyncs, + createUpSyncContent = { + isUsefulUpload = isUsefulUpload || it > 0 EventUpSyncRequestEvent.UpSyncContent(eventDownSyncCount = it) }, ).collect { count -> @@ -121,8 +137,8 @@ internal class EventUpSyncTask @Inject constructor( eventScope = eventScope, project = project, eventScopeTypeToUpload = EventScopeType.UP_SYNC, - batchSize = config.synchronization.up.simprints.batchSizes.upSyncs, - createUpSyncContentContent = { + batchSize = config.synchronization.up.simprints.batchSizes.eventUpSyncs, + createUpSyncContent = { // Only tracking up-sync if there have been ay events in other scopes. EventUpSyncRequestEvent.UpSyncContent( eventUpSyncCount = if (isUsefulUpload) it else 0, @@ -170,7 +186,7 @@ internal class EventUpSyncTask @Inject constructor( eventScopeTypeToUpload: EventScopeType, batchSize: Int, eventFilter: (Map?>) -> Map?> = { it }, - createUpSyncContentContent: (Int) -> EventUpSyncRequestEvent.UpSyncContent, + createUpSyncContent: (Int) -> EventUpSyncRequestEvent.UpSyncContent, ) = flow { Simber.d("Uploading event scope - $eventScopeTypeToUpload in batches of $batchSize", tag = SYNC) @@ -204,7 +220,7 @@ internal class EventUpSyncTask @Inject constructor( eventScope = eventScope, startTime = requestStartTime, result = result, - content = createUpSyncContentContent(this.size), + content = createUpSyncContent(this.size), ) uploadedScopes.addAll(this.map { it.id }) } catch (ex: Exception) { @@ -221,6 +237,7 @@ internal class EventUpSyncTask @Inject constructor( EventScopeType.SESSION -> ApiUploadEventsBody(sessions = this) EventScopeType.DOWN_SYNC -> ApiUploadEventsBody(eventDownSyncs = this) EventScopeType.UP_SYNC -> ApiUploadEventsBody(eventUpSyncs = this) + EventScopeType.SAMPLE_UP_SYNC -> ApiUploadEventsBody(sampleUpSyncs = this) } private fun Map?>.getCorruptedScopes() = filterValues { it == null }.keys @@ -257,7 +274,7 @@ internal class EventUpSyncTask @Inject constructor( result: EventUpSyncResult, content: EventUpSyncRequestEvent.UpSyncContent, ) { - if (content.sessionCount > 0 || content.eventDownSyncCount > 0 || content.eventUpSyncCount > 0) { + if (content.hasAny()) { eventRepository.addOrUpdateEvent( eventScope, EventUpSyncRequestEvent( diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt index 69e6298a4e..38f4474e94 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt @@ -691,6 +691,18 @@ fun validateUpSyncRequestEventApiModel(json: JSONObject) { } } +fun validateSampleUpSyncRequestEventApiModel(json: JSONObject) { + validateCommonParams(json, "SampleUpSyncRequest", 0) + with(json.getJSONObject("payload")) { + validateTimestamp(getJSONObject("startTime")) + validateTimestamp(getJSONObject("endTime")) + assertThat(getString("requestId")).isNotNull() + assertThat(getString("sampleId")).isNotNull() + assertThat(getInt("size")).isNotNull() + assertThat(getString("errorType")).isNotNull() + } +} + fun validateAgeGroupSelectionEventApiModel(json: JSONObject) { validateCommonParams(json, "AgeGroupSelection", 1) with(json.getJSONObject("payload")) { diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/samples/ApiEventSampleUpSyncRequestPayloadTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/samples/ApiEventSampleUpSyncRequestPayloadTest.kt new file mode 100644 index 0000000000..60b11b9e37 --- /dev/null +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/samples/ApiEventSampleUpSyncRequestPayloadTest.kt @@ -0,0 +1,16 @@ +package com.simprints.infra.eventsync.event.remote.models.samples + +import com.google.common.truth.Truth.* +import com.simprints.infra.config.store.models.TokenKeyType +import io.mockk.* +import kotlin.test.Test + +class ApiEventSampleUpSyncRequestPayloadTest { + @Test + fun `when getTokenizedFieldJsonPath is invoked, null is returned`() { + val payload = ApiEventSampleUpSyncRequestPayload(domainPayload = mockk(relaxed = true)) + TokenKeyType.entries.forEach { + assertThat(payload.getTokenizedFieldJsonPath(it)).isNull() + } + } +} diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt index 76692fa06d..f7fa756e4e 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt @@ -51,6 +51,7 @@ import com.simprints.infra.events.sampledata.createOneToOneMatchEvent import com.simprints.infra.events.sampledata.createPersonCreationEvent import com.simprints.infra.events.sampledata.createRefusalCallbackEvent import com.simprints.infra.events.sampledata.createRefusalEvent +import com.simprints.infra.events.sampledata.createSampleUpSyncRequestEvent import com.simprints.infra.events.sampledata.createScannerConnectionEvent import com.simprints.infra.events.sampledata.createScannerFirmwareUpdateEvent import com.simprints.infra.events.sampledata.createSuspiciousIntentEvent @@ -89,6 +90,7 @@ import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.One import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.OneToOneMatch import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.PersonCreation import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Refusal +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.SampleUpSyncRequest import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.ScannerConnection import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.ScannerFirmwareUpdate import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.SuspiciousIntent @@ -124,6 +126,7 @@ import com.simprints.infra.eventsync.event.validateOneToManyMatchEventApiModel import com.simprints.infra.eventsync.event.validateOneToOneMatchEventApiModel import com.simprints.infra.eventsync.event.validatePersonCreationEvent import com.simprints.infra.eventsync.event.validateRefusalEventApiModel +import com.simprints.infra.eventsync.event.validateSampleUpSyncRequestEventApiModel import com.simprints.infra.eventsync.event.validateScannerConnectionEventApiModel import com.simprints.infra.eventsync.event.validateScannerFirmwareUpdateEventApiModel import com.simprints.infra.eventsync.event.validateSuspiciousIntentEventApiModel @@ -566,6 +569,15 @@ internal class MapDomainEventToApiUseCaseTest { validateUpSyncRequestEventApiModel(json) } + @Test + fun validate_SampleUpSyncRequestEventApiModel() { + val event = createSampleUpSyncRequestEvent() + val apiEvent = useCase(event, project) + val json = JSONObject(jackson.writeValueAsString(apiEvent)) + + validateSampleUpSyncRequestEventApiModel(json) + } + @Test fun validate_ageGroupSelectionEventApiModel() { val event = createAgeGroupSelectionEvent() @@ -682,6 +694,7 @@ internal class MapDomainEventToApiUseCaseTest { FaceCaptureBiometrics -> validate_FaceCaptureBiometricsEventApiModel() EventDownSyncRequest -> validate_DownSyncRequestEventApiModel() EventUpSyncRequest -> validate_UpSyncRequestEventApiModel() + SampleUpSyncRequest -> validate_SampleUpSyncRequestEventApiModel() LicenseCheck -> validate_licenseCheckEventApiModel() AgeGroupSelection -> validate_ageGroupSelectionEventApiModel() BiometricReferenceCreation -> validate_biometricReferenceCreationEventApiModel() diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt index 36e407087b..3989dd8ffc 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt @@ -23,6 +23,7 @@ import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_PROJECT_ID import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 import com.simprints.infra.events.sampledata.SampleDefaults.GUID2 import com.simprints.infra.events.sampledata.SampleDefaults.GUID3 +import com.simprints.infra.events.sampledata.SampleDefaults.GUID4 import com.simprints.infra.events.sampledata.createAlertScreenEvent import com.simprints.infra.events.sampledata.createAuthenticationEvent import com.simprints.infra.events.sampledata.createBiometricReferenceCreationEvent @@ -102,9 +103,10 @@ internal class EventUpSyncTaskTest { every { authStore.signedInProjectId } returns DEFAULT_PROJECT_ID every { synchronizationConfiguration.up.simprints.batchSizes } returns UpSynchronizationConfiguration.UpSyncBatchSizes( - 10, - 10, - 10, + sessions = 10, + eventUpSyncs = 10, + eventDownSyncs = 10, + sampleUpSyncs = 10, ) coEvery { configManager.refreshProject(any()) } returns projectWithConfig every { projectWithConfig.project } returns project @@ -150,9 +152,10 @@ internal class EventUpSyncTaskTest { fun `upload events in batches of provided size`() = runTest { setUpSyncKind(UpSynchronizationConfiguration.UpSynchronizationKind.ALL) every { synchronizationConfiguration.up.simprints.batchSizes } returns UpSynchronizationConfiguration.UpSyncBatchSizes( - 2, - 2, - 2, + sessions = 2, + eventUpSyncs = 2, + eventDownSyncs = 2, + sampleUpSyncs = 2, ) coEvery { eventRepo.getClosedEventScopes(any(), any()) } returns emptyList() @@ -176,9 +179,10 @@ internal class EventUpSyncTaskTest { setUpSyncKind(UpSynchronizationConfiguration.UpSynchronizationKind.ALL) val batchSize = 2 every { synchronizationConfiguration.up.simprints.batchSizes } returns UpSynchronizationConfiguration.UpSyncBatchSizes( - batchSize, - batchSize, - batchSize, + sessions = batchSize, + eventUpSyncs = batchSize, + eventDownSyncs = batchSize, + sampleUpSyncs = batchSize, ) coEvery { eventRepo.getClosedEventScopes(any(), any()) } returns emptyList() @@ -204,9 +208,10 @@ internal class EventUpSyncTaskTest { fun `upload out-of-session events in correct fields`() = runTest { setUpSyncKind(UpSynchronizationConfiguration.UpSynchronizationKind.ALL) every { synchronizationConfiguration.up.simprints.batchSizes } returns UpSynchronizationConfiguration.UpSyncBatchSizes( - 2, - 2, - 2, + sessions = 3, + eventUpSyncs = 3, + eventDownSyncs = 3, + sampleUpSyncs = 3, ) coEvery { eventRepo.getClosedEventScopesCount(EventScopeType.SESSION) } returns 0 @@ -220,6 +225,12 @@ internal class EventUpSyncTaskTest { createSessionScope(GUID2), createSessionScope(GUID3), ) + coEvery { eventRepo.getClosedEventScopesCount(EventScopeType.SAMPLE_UP_SYNC) } returns 2 andThen 0 + coEvery { eventRepo.getClosedEventScopes(EventScopeType.SAMPLE_UP_SYNC, any()) } returns listOf( + createSessionScope(GUID1), + createSessionScope(GUID2), + createSessionScope(GUID3), + ) coEvery { eventRepo.getEventsFromScope(any()) } returns listOf(createEventWithSessionId(GUID1, GUID1)) @@ -229,6 +240,7 @@ internal class EventUpSyncTaskTest { coVerify { eventRemoteDataSource.post(any(), any(), match { it.eventDownSyncs.size == 1 }) eventRemoteDataSource.post(any(), any(), match { it.eventUpSyncs.size == 2 }) + eventRemoteDataSource.post(any(), any(), match { it.sampleUpSyncs.size == 3 }) } } @@ -553,6 +565,10 @@ internal class EventUpSyncTaskTest { coEvery { eventRepo.getClosedEventScopes(EventScopeType.DOWN_SYNC, any()) } returns listOf( createSessionScope(GUID2), ) + coEvery { eventRepo.getClosedEventScopesCount(EventScopeType.SAMPLE_UP_SYNC) } returns 1 andThen 0 + coEvery { eventRepo.getClosedEventScopes(EventScopeType.SAMPLE_UP_SYNC, any()) } returns listOf( + createSessionScope(GUID3), + ) coEvery { eventRepo.getEventsFromScope(GUID1) @@ -560,10 +576,13 @@ internal class EventUpSyncTaskTest { coEvery { eventRepo.getEventsFromScope(GUID2) } returns listOf(createEventWithSessionId(GUID2, GUID2)) + coEvery { + eventRepo.getEventsFromScope(GUID3) + } returns listOf(createEventWithSessionId(GUID3, GUID3)) eventUpSyncTask.upSync(operation, eventScope).toList() - coVerify(exactly = 2) { eventRepo.getEventsFromScope(any()) } + coVerify(exactly = 3) { eventRepo.getEventsFromScope(any()) } } @Test @@ -582,14 +601,18 @@ internal class EventUpSyncTaskTest { coEvery { eventRepo.getClosedEventScopes(EventScopeType.DOWN_SYNC, any()) } returns listOf( createSessionScope(GUID3), ) + coEvery { eventRepo.getClosedEventScopesCount(EventScopeType.SAMPLE_UP_SYNC) } returns 1 andThen 0 + coEvery { eventRepo.getClosedEventScopes(EventScopeType.SAMPLE_UP_SYNC, any()) } returns listOf( + createSessionScope(GUID4), + ) coEvery { eventRepo.getEventsFromScope(any()) } returns listOf(createEventWithSessionId(GUID1, GUID1)) eventUpSyncTask.upSync(operation, eventScope).toList() - coVerify(exactly = 3) { eventRepo.getClosedEventScopes(any(), any()) } - coVerify(exactly = 3) { eventRepo.addOrUpdateEvent(any(), any()) } + coVerify(exactly = 4) { eventRepo.getClosedEventScopes(any(), any()) } + coVerify(exactly = 4) { eventRepo.addOrUpdateEvent(any(), any()) } coVerify(exactly = 1) { eventRepo.addOrUpdateEvent( @@ -598,6 +621,12 @@ internal class EventUpSyncTaskTest { it is EventUpSyncRequestEvent && it.payload.content.sessionCount == 1 }, ) + eventRepo.addOrUpdateEvent( + any(), + match { + it is EventUpSyncRequestEvent && it.payload.content.sampleUpSyncCount == 1 + }, + ) eventRepo.addOrUpdateEvent( any(), match { @@ -630,7 +659,7 @@ internal class EventUpSyncTaskTest { } @Test - fun `upload does not reports events if no scopes to upload`() = runTest { + fun `upload does not reports events if no scopes to upload`() = runTest { setUpSyncKind(UpSynchronizationConfiguration.UpSynchronizationKind.ALL) coEvery { eventRepo.getClosedEventScopes(any(), any()) } returns emptyList() diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt index c5a24cf748..d029848cbf 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt @@ -17,7 +17,7 @@ internal class RemoteTestingHelper { ApiEventPayloadType.FaceFallbackCapture, ApiEventPayloadType.FaceCapture, ApiEventPayloadType.FaceCaptureConfirmation, ApiEventPayloadType.FingerprintCaptureBiometrics, ApiEventPayloadType.FaceCaptureBiometrics, ApiEventPayloadType.EventDownSyncRequest, ApiEventPayloadType.EventUpSyncRequest, ApiEventPayloadType.LicenseCheck, - ApiEventPayloadType.AgeGroupSelection, ApiEventPayloadType.BiometricReferenceCreation, + ApiEventPayloadType.AgeGroupSelection, ApiEventPayloadType.BiometricReferenceCreation, ApiEventPayloadType.SampleUpSyncRequest, null, -> { // ADD TEST FOR NEW EVENT IN THIS CLASS diff --git a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt index 7bf3c3694c..ccdacff9c3 100644 --- a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt +++ b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt @@ -78,6 +78,7 @@ import com.simprints.infra.events.event.domain.models.face.FaceFallbackCaptureEv import com.simprints.infra.events.event.domain.models.face.FaceOnboardingCompleteEvent import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureBiometricsEvent import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent +import com.simprints.infra.events.event.domain.models.samples.SampleUpSyncRequestEvent import com.simprints.infra.events.event.domain.models.scope.DatabaseInfo import com.simprints.infra.events.event.domain.models.scope.Device import com.simprints.infra.events.event.domain.models.scope.EventScope @@ -495,11 +496,21 @@ fun createEventUpSyncRequestEvent() = EventUpSyncRequestEvent( sessionCount = 1, eventUpSyncCount = 2, eventDownSyncCount = 3, + sampleUpSyncCount = 4, ), responseStatus = 200, errorType = "OK", ) +fun createSampleUpSyncRequestEvent() = SampleUpSyncRequestEvent( + createdAt = CREATED_AT, + endedAt = ENDED_AT, + requestId = GUID1, + sampleId = GUID2, + size = 100, + errorType = "OK", +) + fun createLicenseCheckEvent() = LicenseCheckEvent( createdAt = CREATED_AT, status = LicenseCheckEvent.LicenseStatus.VALID, diff --git a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/SampleDefaults.kt b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/SampleDefaults.kt index 8f208db76e..677831bd2d 100644 --- a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/SampleDefaults.kt +++ b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/SampleDefaults.kt @@ -23,6 +23,7 @@ object SampleDefaults { val GUID1 = UUID.randomUUID().toString() val GUID2 = UUID.randomUUID().toString() val GUID3 = UUID.randomUUID().toString() + val GUID4 = UUID.randomUUID().toString() val TIME1 = System.currentTimeMillis() diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt index dde7444623..66119a3c73 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt @@ -49,6 +49,7 @@ import com.simprints.infra.events.event.domain.models.EventType.Companion.ONE_TO import com.simprints.infra.events.event.domain.models.EventType.Companion.ONE_TO_ONE_MATCH_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.PERSON_CREATION_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.REFUSAL_KEY +import com.simprints.infra.events.event.domain.models.EventType.Companion.SAMPLE_UP_SYNC_REQUEST import com.simprints.infra.events.event.domain.models.EventType.Companion.SCANNER_CONNECTION_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.SCANNER_FIRMWARE_UPDATE_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.SUSPICIOUS_INTENT_KEY @@ -77,6 +78,7 @@ import com.simprints.infra.events.event.domain.models.face.FaceFallbackCaptureEv import com.simprints.infra.events.event.domain.models.face.FaceOnboardingCompleteEvent import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureBiometricsEvent import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent +import com.simprints.infra.events.event.domain.models.samples.SampleUpSyncRequestEvent import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestEvent @JsonTypeInfo( @@ -134,6 +136,7 @@ import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestE JsonSubTypes.Type(value = LicenseCheckEvent::class, name = LICENSE_CHECK_KEY), JsonSubTypes.Type(value = AgeGroupSelectionEvent::class, name = AGE_GROUP_SELECTION_KEY), JsonSubTypes.Type(value = BiometricReferenceCreationEvent::class, name = BIOMETRIC_REFERENCE_CREATION_KEY), + JsonSubTypes.Type(value = SampleUpSyncRequestEvent::class, name = SAMPLE_UP_SYNC_REQUEST), ) abstract class Event { abstract val id: String diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt index 12c1fd5f1d..9cd815e5f1 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt @@ -49,6 +49,7 @@ import com.simprints.infra.events.event.domain.models.face.FaceFallbackCaptureEv import com.simprints.infra.events.event.domain.models.face.FaceOnboardingCompleteEvent.FaceOnboardingCompletePayload import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureBiometricsEvent.FingerprintCaptureBiometricsPayload import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent.FingerprintCapturePayload +import com.simprints.infra.events.event.domain.models.samples.SampleUpSyncRequestEvent.SampleUpSyncRequestPayload import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestEvent.EventUpSyncRequestPayload @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true) @@ -63,10 +64,22 @@ import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestE JsonSubTypes.Type(value = ConfirmationCalloutEventV3.ConfirmationCalloutPayload::class, name = EventType.CALLOUT_CONFIRMATION_V3_KEY), JsonSubTypes.Type(value = EnrolmentCalloutEventV2.EnrolmentCalloutPayload::class, name = EventType.CALLOUT_ENROLMENT_KEY), JsonSubTypes.Type(value = EnrolmentCalloutEventV3.EnrolmentCalloutPayload::class, name = EventType.CALLOUT_ENROLMENT_V3_KEY), - JsonSubTypes.Type(value = EnrolmentLastBiometricsCalloutEventV2.EnrolmentLastBiometricsCalloutPayload::class, name = EventType.CALLOUT_LAST_BIOMETRICS_KEY), - JsonSubTypes.Type(value = EnrolmentLastBiometricsCalloutEventV3.EnrolmentLastBiometricsCalloutPayload::class, name = EventType.CALLOUT_LAST_BIOMETRICS_V3_KEY), - JsonSubTypes.Type(value = IdentificationCalloutEventV2.IdentificationCalloutPayload::class, name = EventType.CALLOUT_IDENTIFICATION_KEY), - JsonSubTypes.Type(value = IdentificationCalloutEventV3.IdentificationCalloutPayload::class, name = EventType.CALLOUT_IDENTIFICATION_V3_KEY), + JsonSubTypes.Type( + value = EnrolmentLastBiometricsCalloutEventV2.EnrolmentLastBiometricsCalloutPayload::class, + name = EventType.CALLOUT_LAST_BIOMETRICS_KEY, + ), + JsonSubTypes.Type( + value = EnrolmentLastBiometricsCalloutEventV3.EnrolmentLastBiometricsCalloutPayload::class, + name = EventType.CALLOUT_LAST_BIOMETRICS_V3_KEY, + ), + JsonSubTypes.Type( + value = IdentificationCalloutEventV2.IdentificationCalloutPayload::class, + name = EventType.CALLOUT_IDENTIFICATION_KEY, + ), + JsonSubTypes.Type( + value = IdentificationCalloutEventV3.IdentificationCalloutPayload::class, + name = EventType.CALLOUT_IDENTIFICATION_V3_KEY, + ), JsonSubTypes.Type(value = VerificationCalloutEventV2.VerificationCalloutPayload::class, name = EventType.CALLOUT_VERIFICATION_KEY), JsonSubTypes.Type(value = VerificationCalloutEventV3.VerificationCalloutPayload::class, name = EventType.CALLOUT_VERIFICATION_V3_KEY), JsonSubTypes.Type(value = FaceCaptureConfirmationPayload::class, name = EventType.FACE_CAPTURE_CONFIRMATION_KEY), @@ -101,6 +114,7 @@ import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestE JsonSubTypes.Type(value = LicenseCheckEventPayload::class, name = Companion.LICENSE_CHECK_KEY), JsonSubTypes.Type(value = AgeGroupSelectionPayload::class, name = Companion.AGE_GROUP_SELECTION_KEY), JsonSubTypes.Type(value = BiometricReferenceCreationPayload::class, name = Companion.BIOMETRIC_REFERENCE_CREATION_KEY), + JsonSubTypes.Type(value = SampleUpSyncRequestPayload::class, name = Companion.SAMPLE_UP_SYNC_REQUEST), ) abstract class EventPayload { abstract val type: EventType diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt index aa7bf0ff56..505b0b4a88 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt @@ -143,6 +143,9 @@ enum class EventType { // key added: EVENT_UP_SYNC_REQUEST_KEY EVENT_UP_SYNC_REQUEST, + // key added: SAMPLE_UP_SYNC_REQUEST + SAMPLE_UP_SYNC_REQUEST, + // key added: LICENSE_CHECK_KEY LICENSE_CHECK, @@ -199,6 +202,7 @@ enum class EventType { const val VERO_2_INFO_SNAPSHOT_KEY = "VERO_2_INFO_SNAPSHOT" const val EVENT_DOWN_SYNC_REQUEST_KEY = "EVENT_DOWN_SYNC_REQUEST" const val EVENT_UP_SYNC_REQUEST_KEY = "EVENT_UP_SYNC_REQUEST" + const val SAMPLE_UP_SYNC_REQUEST = "SAMPLE_UP_SYNC_REQUEST" const val LICENSE_CHECK_KEY = "LICENSE_CHECK" const val AGE_GROUP_SELECTION_KEY = "AGE_GROUP_SELECTION" const val BIOMETRIC_REFERENCE_CREATION_KEY = "BIOMETRIC_REFERENCE_CREATION" diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/samples/SampleUpSyncRequestEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/samples/SampleUpSyncRequestEvent.kt new file mode 100644 index 0000000000..bbf8b43bd0 --- /dev/null +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/samples/SampleUpSyncRequestEvent.kt @@ -0,0 +1,63 @@ +package com.simprints.infra.events.event.domain.models.samples + +import androidx.annotation.Keep +import com.simprints.core.domain.tokenization.TokenizableString +import com.simprints.core.tools.time.Timestamp +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.Event +import com.simprints.infra.events.event.domain.models.EventPayload +import com.simprints.infra.events.event.domain.models.EventType +import java.util.UUID + +@Keep +class SampleUpSyncRequestEvent( + override val id: String = UUID.randomUUID().toString(), + override val payload: SampleUpSyncRequestPayload, + override val type: EventType, + override var scopeId: String? = null, + override var projectId: String? = null, +) : Event() { + constructor( + createdAt: Timestamp, + endedAt: Timestamp, + requestId: String, + sampleId: String, + size: Int, + errorType: String? = null, + ) : this( + UUID.randomUUID().toString(), + SampleUpSyncRequestPayload( + createdAt, + endedAt, + requestId, + sampleId, + size, + errorType, + EVENT_VERSION, + ), + EventType.SAMPLE_UP_SYNC_REQUEST, + ) + + @Keep + data class SampleUpSyncRequestPayload( + override val createdAt: Timestamp, + override val endedAt: Timestamp?, + val requestId: String, + val sampleId: String, + val size: Int, + val errorType: String?, + override val eventVersion: Int, + override val type: EventType = EventType.SAMPLE_UP_SYNC_REQUEST, + ) : EventPayload() { + override fun toSafeString(): String = "request ID: $requestId, error: $errorType," + + "sample: $sampleId, sample size: $size" + } + + override fun getTokenizableFields(): Map = emptyMap() + + override fun setTokenizedFields(map: Map): Event = this + + companion object Companion { + const val EVENT_VERSION = 0 + } +} diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/scope/EventScopeType.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/scope/EventScopeType.kt index 2214b5f0ae..7b7aeb3cef 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/scope/EventScopeType.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/scope/EventScopeType.kt @@ -7,4 +7,5 @@ enum class EventScopeType { SESSION, UP_SYNC, DOWN_SYNC, + SAMPLE_UP_SYNC, } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/upsync/EventUpSyncRequestEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/upsync/EventUpSyncRequestEvent.kt index 4da88e355b..55d7ce03a6 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/upsync/EventUpSyncRequestEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/upsync/EventUpSyncRequestEvent.kt @@ -50,7 +50,8 @@ data class EventUpSyncRequestEvent( override val type: EventType = EventType.EVENT_UP_SYNC_REQUEST, ) : EventPayload() { override fun toSafeString(): String = "request ID: $requestId, response: $responseStatus, error: $errorType," + - "sessions: ${content.sessionCount}, eventsUp: ${content.eventUpSyncCount}, eventsDown: ${content.eventDownSyncCount}" + "sessions: ${content.sessionCount}, eventsUp: ${content.eventUpSyncCount}, " + + "eventsDown: ${content.eventDownSyncCount}, samples: ${content.sampleUpSyncCount}" } @Keep @@ -58,7 +59,10 @@ data class EventUpSyncRequestEvent( val sessionCount: Int = 0, val eventUpSyncCount: Int = 0, val eventDownSyncCount: Int = 0, - ) + val sampleUpSyncCount: Int = 0, + ) { + fun hasAny() = sessionCount > 0 || eventUpSyncCount > 0 || eventDownSyncCount > 0 || sampleUpSyncCount > 0 + } override fun getTokenizableFields(): Map = emptyMap() diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/local/models/DbEventTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/local/models/DbEventTest.kt index 2679cb3a94..e210fda3c2 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/local/models/DbEventTest.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/local/models/DbEventTest.kt @@ -312,4 +312,12 @@ class DbEventTest { assertThat(original).isEqualTo(transformed) } + + @Test + fun convert_SampleUpSyncRequestEvent() { + val original = createSampleUpSyncRequestEvent() + val transformed = original.fromDomainToDb().fromDbToDomain() + + assertThat(original).isEqualTo(transformed) + } } diff --git a/infra/events/src/test/resources/all-events/sample_up_sync_request_v0.json b/infra/events/src/test/resources/all-events/sample_up_sync_request_v0.json new file mode 100644 index 0000000000..0d4245ee9d --- /dev/null +++ b/infra/events/src/test/resources/all-events/sample_up_sync_request_v0.json @@ -0,0 +1,19 @@ +{ + "id": "c1ed20db-dc70-42c4-8449-725f63c6cf8e", + "type": "SAMPLE_UP_SYNC_REQUEST", + "labels": { + "projectId": "TEST6Oai41ps1pBNrzBL", + "sessionId": "e35c39f9-b81e-48f2-97e7-46ecc8399bb4", + "deviceId": "f2fd8393c0a0be67" + }, + "payload": { + "type": "SAMPLE_UP_SYNC_REQUEST", + "eventVersion": 0, + "createdAt": 221675670, + "endedAt": 9141278930, + "requestId": "e35c39f9-b81e-48f2-97e7-46ecc8399bb4", + "sampleId": "67463998-b9da-4eb5-bdef-8057bcbc1ebe", + "size": 100, + "errorType": "java.lang.NullPointerException" + } +} diff --git a/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt b/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt index 6ec2357a1a..a0a228f623 100644 --- a/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt +++ b/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt @@ -14,6 +14,7 @@ import com.simprints.infra.config.store.models.MaxCaptureAttempts import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.ProjectState +import com.simprints.infra.config.store.models.SampleSynchronizationConfiguration import com.simprints.infra.config.store.models.SettingsPasswordConfig import com.simprints.infra.config.store.models.SynchronizationConfiguration import com.simprints.infra.config.store.models.TokenKeyType @@ -117,6 +118,7 @@ internal val synchronizationConfiguration = SynchronizationConfiguration( listOf("module1".asTokenizableEncrypted()), "PT24H", ), + SampleSynchronizationConfiguration(3), ) internal val identificationConfiguration =