From 37a4f69422112944c8ed16a2339d98cdf385b64f Mon Sep 17 00:00:00 2001
From: BMItter <46747780+BMItter@users.noreply.github.com>
Date: Fri, 5 Mar 2021 15:41:16 +0100
Subject: [PATCH] V2 Configuration Options for Risk Calculation
 (EXPOSUREAPP-5217) (#2512)

* Drop exposure window when no risklevel is associated - wip

* improved logging in DefaultRiskLevels

* Adapted new trasmission risk level calc

* Added placeholder for transmission risk value mapping

* Adjusted calculation test, updated test data

* Updated path for appconfig v2

* Replaced Transmission Risk Level Multiplier with Transmission Risk Value Mapping

* cleanUp

* Use protobufs

* better readable risk calculation parameters

* Updated default config

* adjusted tests

* Return null instead of throwing an exception

* Removed obsolete exceptions

Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com>
---
 ...iskLevelCalculationFragmentCWAViewModel.kt |  25 +-
 .../assets/default_app_config_android.bin     | Bin 558 -> 674 bytes
 .../assets/default_app_config_android.sha256  |   2 +-
 .../ExposureWindowRiskCalculationConfig.kt    |   2 +-
 .../appconfig/download/AppConfigApiV2.kt      |   2 +-
 ...posureWindowRiskCalculationConfigMapper.kt |  29 +-
 .../coronawarnapp/risk/DefaultRiskLevels.kt   |  78 ++--
 .../fallback/DefaultAppConfigSanityCheck.kt   |   2 +-
 .../fallback/DefaultAppConfigSourceTest.kt    |   2 +
 .../sources/remote/AppConfigApiTest.kt        |   2 +-
 .../windows/ExposureWindowsCalculationTest.kt |  25 +-
 .../DefaultRiskCalculationConfiguration.kt    |   6 +-
 .../JsonTransmissionRiskValueMapping.kt       |  10 +
 .../exposure-windows-risk-calculation.json    | 336 ++++++++++++------
 14 files changed, 320 insertions(+), 201 deletions(-)
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTransmissionRiskValueMapping.kt

diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
index 75beae1cd..cd78b9e7e 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
@@ -9,7 +9,6 @@ import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.appconfig.AppConfigProvider
-import de.rki.coronawarnapp.appconfig.ConfigData
 import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysSettings
 import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
 import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
@@ -109,31 +108,9 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
 
     val backendParameters = appConfigProvider
         .currentConfig
-        .map { it.toReadableString() }
+        .map { it.rawConfig.riskCalculationParameters.toString() }
         .asLiveData()
 
-    private fun ConfigData.toReadableString(): String = StringBuilder()
-        .appendLine("Transmission RiskLevel Multiplier: $transmissionRiskLevelMultiplier")
-        .appendLine()
-        .appendLine("Minutes At Attenuation Filters:")
-        .appendLine(minutesAtAttenuationFilters)
-        .appendLine()
-        .appendLine("Minutes At Attenuation Weights:")
-        .appendLine(minutesAtAttenuationWeights)
-        .appendLine()
-        .appendLine("Transmission RiskLevel Encoding:")
-        .appendLine(transmissionRiskLevelEncoding)
-        .appendLine()
-        .appendLine("Transmission RiskLevel Filters:")
-        .appendLine(transmissionRiskLevelFilters)
-        .appendLine()
-        .appendLine("Normalized Time Per Exposure Window To RiskLevel Mapping:")
-        .appendLine(normalizedTimePerExposureWindowToRiskLevelMapping)
-        .appendLine()
-        .appendLine("Normalized Time Per Day To RiskLevel Mapping List:")
-        .appendLine(normalizedTimePerDayToRiskLevelMappingList)
-        .toString()
-
     val additionalRiskCalcInfo = combine(
         riskLevelStorage.latestAndLastSuccessful,
         exposureDetectionTracker.latestSubmission()
diff --git a/Corona-Warn-App/src/main/assets/default_app_config_android.bin b/Corona-Warn-App/src/main/assets/default_app_config_android.bin
index 71db85788c2beff255f07410e14f0ec2bbea4bbb..aeac5e2d45c54db350f6219cbc4487ee8c691af0 100644
GIT binary patch
delta 234
zcmZ3-vWRtp7*iwjM2V|RA`TPZTh%i_ffF|eqaYH837NwzXbc9A?SaZz1ZT|zftMf-
zD^%GB5Qhz_>@$eNE|>-e-#{D=sGgtpP6Avk9E<{tK$1y-Nno-AW4nVrmoXP}eo27<
zqm))gNl8JmmA-ybYFbfZdTCyIYMx$EcBWoRs(x{4QCVuGZeC(;s%}YAiVz0}g8+jB
Sg95{3P9{~pBpxmXgb)C;Z8Kf~

delta 79
zcmZ3)x{hUn7}HFqi4s>QZWWyPPn&(#Ob|FZc?RQj9!D-yF6R7_0s#gooypcr($dc1
hj!ucinZ-$|X+??YrFrQ>92|@Sj1mkAjFa=3Q~_pK7_tBW

diff --git a/Corona-Warn-App/src/main/assets/default_app_config_android.sha256 b/Corona-Warn-App/src/main/assets/default_app_config_android.sha256
index dcbdbc7b8..bd4fdf7c0 100644
--- a/Corona-Warn-App/src/main/assets/default_app_config_android.sha256
+++ b/Corona-Warn-App/src/main/assets/default_app_config_android.sha256
@@ -1 +1 @@
-12d0b93c0c02c6870ef75c173a53a8ffb9cab6828fbf22e751053329c425eef2
\ No newline at end of file
+3d108b3fee7d1b4c227087c82bb804048de8d0542c3f2b26cf507a918201124d
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt
index cf72d970a..f772fa17e 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt
@@ -9,11 +9,11 @@ interface ExposureWindowRiskCalculationConfig {
     val minutesAtAttenuationWeights: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>
     val transmissionRiskLevelEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding
     val transmissionRiskLevelFilters: List<RiskCalculationParametersOuterClass.TrlFilter>
-    val transmissionRiskLevelMultiplier: Double
     val normalizedTimePerExposureWindowToRiskLevelMapping:
         List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>
     val normalizedTimePerDayToRiskLevelMappingList:
         List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>
+    val transmissionRiskValueMapping: List<RiskCalculationParametersOuterClass.TransmissionRiskValueMapping>
     val diagnosisKeysDataMapping: DiagnosisKeysDataMapping
 
     interface Mapper {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV2.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV2.kt
index 5d1c0f2c3..18c6e2509 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV2.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV2.kt
@@ -6,6 +6,6 @@ import retrofit2.http.GET
 
 interface AppConfigApiV2 {
 
-    @GET("/version/v1/app_config_android")
+    @GET("/version/v2/app_config_android")
     suspend fun getApplicationConfiguration(): Response<ResponseBody>
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt
index 460187ecc..03ab77237 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt
@@ -28,20 +28,15 @@ class ExposureWindowRiskCalculationConfigMapper @Inject constructor() :
         val riskCalculationParameters = rawConfig.riskCalculationParameters
 
         return ExposureWindowRiskCalculationContainer(
-            minutesAtAttenuationFilters = riskCalculationParameters
-                .minutesAtAttenuationFiltersList,
-            minutesAtAttenuationWeights = riskCalculationParameters
-                .minutesAtAttenuationWeightsList,
-            transmissionRiskLevelEncoding = riskCalculationParameters
-                .trlEncoding,
-            transmissionRiskLevelFilters = riskCalculationParameters
-                .trlFiltersList,
-            transmissionRiskLevelMultiplier = riskCalculationParameters
-                .transmissionRiskLevelMultiplier,
-            normalizedTimePerExposureWindowToRiskLevelMapping = riskCalculationParameters
-                .normalizedTimePerEWToRiskLevelMappingList,
-            normalizedTimePerDayToRiskLevelMappingList = riskCalculationParameters
-                .normalizedTimePerDayToRiskLevelMappingList,
+            minutesAtAttenuationFilters = riskCalculationParameters.minutesAtAttenuationFiltersList,
+            minutesAtAttenuationWeights = riskCalculationParameters.minutesAtAttenuationWeightsList,
+            transmissionRiskLevelEncoding = riskCalculationParameters.trlEncoding,
+            transmissionRiskLevelFilters = riskCalculationParameters.trlFiltersList,
+            normalizedTimePerExposureWindowToRiskLevelMapping =
+                riskCalculationParameters.normalizedTimePerEWToRiskLevelMappingList,
+            normalizedTimePerDayToRiskLevelMappingList =
+                riskCalculationParameters.normalizedTimePerDayToRiskLevelMappingList,
+            transmissionRiskValueMapping = riskCalculationParameters.transmissionRiskValueMappingList,
             diagnosisKeysDataMapping = rawConfig.diagnosisKeysDataMapping()
         )
     }
@@ -63,12 +58,14 @@ class ExposureWindowRiskCalculationConfigMapper @Inject constructor() :
         override val minutesAtAttenuationFilters: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter>,
         override val minutesAtAttenuationWeights: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>,
         override val transmissionRiskLevelEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding,
-        override val transmissionRiskLevelFilters: List<RiskCalculationParametersOuterClass.TrlFilter>,
-        override val transmissionRiskLevelMultiplier: Double,
+        override val transmissionRiskLevelFilters:
+            List<RiskCalculationParametersOuterClass.TrlFilter>,
         override val normalizedTimePerExposureWindowToRiskLevelMapping:
             List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>,
         override val normalizedTimePerDayToRiskLevelMappingList:
             List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>,
+        override val transmissionRiskValueMapping:
+            List<RiskCalculationParametersOuterClass.TransmissionRiskValueMapping>,
         override val diagnosisKeysDataMapping: DiagnosisKeysDataMapping
     ) : ExposureWindowRiskCalculationConfig
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt
index 734d7ff3d..85fd30cce 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt
@@ -1,6 +1,5 @@
 package de.rki.coronawarnapp.risk
 
-import android.text.TextUtils
 import com.google.android.gms.nearby.exposurenotification.ExposureWindow
 import com.google.android.gms.nearby.exposurenotification.Infectiousness
 import com.google.android.gms.nearby.exposurenotification.ReportType
@@ -114,8 +113,9 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
             return null
         }
 
-        val transmissionRiskValue: Double =
-            transmissionRiskLevel * appConfig.transmissionRiskLevelMultiplier
+        val transmissionRiskValue: Double = appConfig.transmissionRiskValueMapping
+            .find { it.transmissionRiskLevel == transmissionRiskLevel }
+            ?.transmissionRiskValue ?: 0.0
 
         Timber.d("%s's transmissionRiskValue is: %s", exposureWindow, transmissionRiskValue)
 
@@ -135,8 +135,12 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
         )
 
         if (riskLevel == null) {
-            Timber.e("Exposure Window: $exposureWindow could not be mapped to a risk level")
-            throw NormalizedTimePerExposureWindowToRiskLevelMappingMissingException()
+            Timber.d(
+                "%s dropped due to risk level filter is %s",
+                exposureWindow,
+                riskLevel
+            )
+            return null
         }
 
         Timber.d("%s's riskLevel is: %s", exposureWindow, riskLevel)
@@ -158,13 +162,13 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
 
         Timber.d(
             "uniqueDates: %s",
-            { TextUtils.join(System.lineSeparator(), uniqueDatesMillisSinceEpoch) }
+            uniqueDatesMillisSinceEpoch
         )
-        val exposureHistory = uniqueDatesMillisSinceEpoch.map {
+        val exposureHistory = uniqueDatesMillisSinceEpoch.mapNotNull {
             aggregateRiskPerDate(appConfig, it, exposureWindowResultMap)
         }
 
-        Timber.d("exposureHistory size: ${exposureHistory.size}")
+        Timber.d("exposureHistory size: %d", exposureHistory.size)
 
         // 6. Determine `Total Risk`
         val totalRiskLevel =
@@ -180,43 +184,43 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
                 RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW
             }
 
-        Timber.d("totalRiskLevel: ${totalRiskLevel.name} (${totalRiskLevel.ordinal})")
+        Timber.d("totalRiskLevel: %s (%d)", totalRiskLevel.name, totalRiskLevel.ordinal)
 
         // 7. Determine `Date of Most Recent Date with Low Risk`
         val mostRecentDateWithLowRisk =
             exposureHistory.mostRecentDateForRisk(ProtoRiskLevel.LOW)
 
-        Timber.d("mostRecentDateWithLowRisk: $mostRecentDateWithLowRisk")
+        Timber.d("mostRecentDateWithLowRisk: %s", mostRecentDateWithLowRisk)
 
         // 8. Determine `Date of Most Recent Date with High Risk`
         val mostRecentDateWithHighRisk =
             exposureHistory.mostRecentDateForRisk(ProtoRiskLevel.HIGH)
 
-        Timber.d("mostRecentDateWithHighRisk: $mostRecentDateWithHighRisk")
+        Timber.d("mostRecentDateWithHighRisk: %s", mostRecentDateWithHighRisk)
 
         // 9. Determine `Total Minimum Distinct Encounters With Low Risk`
         val totalMinimumDistinctEncountersWithLowRisk = exposureHistory
             .sumBy { it.minimumDistinctEncountersWithLowRisk }
 
-        Timber.d("totalMinimumDistinctEncountersWithLowRisk: $totalMinimumDistinctEncountersWithLowRisk")
+        Timber.d("totalMinimumDistinctEncountersWithLowRisk: %d", totalMinimumDistinctEncountersWithLowRisk)
 
         // 10. Determine `Total Minimum Distinct Encounters With High Risk`
         val totalMinimumDistinctEncountersWithHighRisk = exposureHistory
             .sumBy { it.minimumDistinctEncountersWithHighRisk }
 
-        Timber.d("totalMinimumDistinctEncountersWithHighRisk: $totalMinimumDistinctEncountersWithHighRisk")
+        Timber.d("totalMinimumDistinctEncountersWithHighRisk: %d", totalMinimumDistinctEncountersWithHighRisk)
 
         // 11. Determine `Number of Days With Low Risk`
         val numberOfDaysWithLowRisk =
             exposureHistory.numberOfDaysForRisk(ProtoRiskLevel.LOW)
 
-        Timber.d("numberOfDaysWithLowRisk: $numberOfDaysWithLowRisk")
+        Timber.d("numberOfDaysWithLowRisk: %d", numberOfDaysWithLowRisk)
 
         // 12. Determine `Number of Days With High Risk`
         val numberOfDaysWithHighRisk =
             exposureHistory.numberOfDaysForRisk(ProtoRiskLevel.HIGH)
 
-        Timber.d("numberOfDaysWithHighRisk: $numberOfDaysWithHighRisk")
+        Timber.d("numberOfDaysWithHighRisk: %d", numberOfDaysWithHighRisk)
 
         return AggregatedRiskResult(
             totalRiskLevel = totalRiskLevel,
@@ -243,7 +247,7 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
         appConfig: ExposureWindowRiskCalculationConfig,
         dateMillisSinceEpoch: Long,
         exposureWindowsAndResult: Map<ExposureWindow, RiskResult>
-    ): AggregatedRiskPerDateResult {
+    ): AggregatedRiskPerDateResult? {
         // 1. Group `Exposure Windows by Date`
         val exposureWindowsAndResultForDate = exposureWindowsAndResult
             .filter { it.key.dateMillisSinceEpoch == dateMillisSinceEpoch }
@@ -252,31 +256,40 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
         val normalizedTime = exposureWindowsAndResultForDate.values
             .sumOf { it.normalizedTime }
 
-        Timber.d("Aggregating result for date $dateMillisSinceEpoch - ${Instant.ofEpochMilli(dateMillisSinceEpoch)}")
+        Timber.d(
+            "Aggregating result for date %d - %s",
+            dateMillisSinceEpoch,
+            Instant.ofEpochMilli(dateMillisSinceEpoch)
+        )
 
         // 3. Determine `Risk Level per Date`
-        val riskLevel = try {
-            appConfig.normalizedTimePerDayToRiskLevelMappingList
-                .filter { it.normalizedTimeRange.inRange(normalizedTime) }
-                .map { it.riskLevel }
-                .first()
-        } catch (e: Exception) {
-            throw NormalizedTimePerDayToRiskLevelMappingMissingException()
+        val riskLevel = appConfig.normalizedTimePerDayToRiskLevelMappingList
+            .filter { it.normalizedTimeRange.inRange(normalizedTime) }
+            .map { it.riskLevel }
+            .firstOrNull()
+
+        if (riskLevel == null) {
+            Timber.d(
+                "No Risk Level is associated with date %d - %s",
+                dateMillisSinceEpoch,
+                Instant.ofEpochMilli(dateMillisSinceEpoch)
+            )
+            return null
         }
 
-        Timber.d("riskLevel: ${riskLevel.name} (${riskLevel.ordinal})")
+        Timber.d("riskLevel: %s (%d)", riskLevel.name, riskLevel.ordinal)
 
         // 4. Determine `Minimum Distinct Encounters With Low Risk per Date`
         val minimumDistinctEncountersWithLowRisk =
             exposureWindowsAndResultForDate.minimumDistinctEncountersForRisk(ProtoRiskLevel.LOW)
 
-        Timber.d("minimumDistinctEncountersWithLowRisk: $minimumDistinctEncountersWithLowRisk")
+        Timber.d("minimumDistinctEncountersWithLowRisk: %d", minimumDistinctEncountersWithLowRisk)
 
         // 5. Determine `Minimum Distinct Encounters With High Risk per Date`
         val minimumDistinctEncountersWithHighRisk =
             exposureWindowsAndResultForDate.minimumDistinctEncountersForRisk(ProtoRiskLevel.HIGH)
 
-        Timber.d("minimumDistinctEncountersWithHighRisk: $minimumDistinctEncountersWithHighRisk")
+        Timber.d("minimumDistinctEncountersWithHighRisk: %d", minimumDistinctEncountersWithHighRisk)
 
         return AggregatedRiskPerDateResult(
             dateMillisSinceEpoch = dateMillisSinceEpoch,
@@ -293,17 +306,6 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
             .size
 
     companion object {
-
-        open class RiskLevelMappingMissingException(msg: String) : Exception(msg)
-
-        class NormalizedTimePerExposureWindowToRiskLevelMappingMissingException : RiskLevelMappingMissingException(
-            "Failed to map the normalized Time per Exposure Window to a Risk Level"
-        )
-
-        class NormalizedTimePerDayToRiskLevelMappingMissingException : RiskLevelMappingMissingException(
-            "Failed to map the normalized Time per Day to a Risk Level"
-        )
-
         class UnknownReportTypeException : Exception(
             "The Report Type returned by the ENF is not known"
         )
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt
index 3c94d8afd..67679862b 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt
@@ -37,7 +37,7 @@ class DefaultAppConfigSanityCheck : BaseTest() {
     fun `current default matches checksum`() {
         val config = context.assets.open(configName).readBytes()
         val sha256 = context.assets.open(checkSumName).readBytes().toString(Charsets.UTF_8)
-        sha256 shouldBe "12d0b93c0c02c6870ef75c173a53a8ffb9cab6828fbf22e751053329c425eef2"
+        sha256 shouldBe "3d108b3fee7d1b4c227087c82bb804048de8d0542c3f2b26cf507a918201124d"
         config.toSHA256() shouldBe sha256
     }
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSourceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSourceTest.kt
index 313f4cbca..cf21c6b07 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSourceTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSourceTest.kt
@@ -20,6 +20,7 @@ import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseIOTest
 import java.io.File
+import java.io.FileNotFoundException
 
 class DefaultAppConfigSourceTest : BaseIOTest() {
     @MockK private lateinit var context: Context
@@ -81,6 +82,7 @@ class DefaultAppConfigSourceTest : BaseIOTest() {
 
     @Test
     fun `exceptions when getting the default config are rethrown`() = runBlockingTest {
+        every { assetManager.open("default_app_config_android.bin") } throws FileNotFoundException("default_app_config_android.bin does not exist")
         val instance = createInstance()
 
         shouldThrowAny {
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigApiTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigApiTest.kt
index 7534816f0..5f9f1e884 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigApiTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigApiTest.kt
@@ -75,6 +75,6 @@ class AppConfigApiTest : BaseIOTest() {
 
         val request = webServer.takeRequest(5, TimeUnit.SECONDS)!!
         request.method shouldBe "GET"
-        request.path shouldBe "/version/v1/app_config_android"
+        request.path shouldBe "/version/v2/app_config_android"
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt
index 0e88002d6..d02dac3c4 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt
@@ -14,6 +14,7 @@ import de.rki.coronawarnapp.nearby.windows.entities.configuration.DefaultRiskCal
 import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonMinutesAtAttenuationFilter
 import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonMinutesAtAttenuationWeight
 import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonNormalizedTimeToRiskLevelMapping
+import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonTransmissionRiskValueMapping
 import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonTrlFilter
 import de.rki.coronawarnapp.risk.DefaultRiskLevels
 import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
@@ -261,8 +262,14 @@ class ExposureWindowsCalculationTest : BaseTest() {
             result.append(logRange(filter.dropIfTrlInRange, "Drop If Trl In Range"))
         }
 
-        result.append("\n").append("â—¦ Transmission Risk Level Multiplier: ${config.transmissionRiskLevelMultiplier}")
-        result.append("\n").append("-------------------------------------------- âš™ -").append("\n")
+        result.append("\n").appendLine("â—¦ Transmission Risk Value Mapping (${config.transmissionRiskValueMapping.size})")
+        for (mapping in config.transmissionRiskValueMapping) {
+            result.append("\t").appendLine("⇥ Mapping")
+            result.append("\t\t").append("↳ transmissionRiskLevel: ").appendLine(mapping.transmissionRiskLevel)
+            result.append("\t\t").append("↳ transmissionRiskValue: ").appendLine(mapping.transmissionRiskValue)
+        }
+
+        result.appendLine("-------------------------------------------- âš™ -")
         debugLog(result.toString(), LogLevel.NONE)
     }
 
@@ -364,8 +371,6 @@ class ExposureWindowsCalculationTest : BaseTest() {
         }
         every { testConfig.normalizedTimePerExposureWindowToRiskLevelMapping } returns normalizedTimePerExposureWindowToRiskLevelMapping
 
-        every { testConfig.transmissionRiskLevelMultiplier } returns json.transmissionRiskLevelMultiplier
-
         val trlEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding = mockk()
         every { trlEncoding.infectiousnessOffsetHigh } returns json.trlEncoding.infectiousnessOffsetHigh
         every { trlEncoding.infectiousnessOffsetStandard } returns json.trlEncoding.infectiousnessOffsetStandard
@@ -385,6 +390,18 @@ class ExposureWindowsCalculationTest : BaseTest() {
             trlFilters.add(filter)
         }
         every { testConfig.transmissionRiskLevelFilters } returns trlFilters
+
+        val transmissionRiskValueMapping =
+            mutableListOf<RiskCalculationParametersOuterClass.TransmissionRiskValueMapping>()
+        for (jsonMapping: JsonTransmissionRiskValueMapping in json.transmissionRiskValueMapping) {
+            val mapping: RiskCalculationParametersOuterClass.TransmissionRiskValueMapping = mockk()
+            mapping.run {
+                every { transmissionRiskLevel } returns jsonMapping.transmissionRiskLevel
+                every { transmissionRiskValue } returns jsonMapping.transmissionRiskValue
+            }
+            transmissionRiskValueMapping += mapping
+        }
+        every { testConfig.transmissionRiskValueMapping } returns transmissionRiskValueMapping
     }
 
     private fun jsonToExposureWindow(json: JsonWindow): ExposureWindow {
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt
index 7445d86e7..1273b327d 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt
@@ -11,10 +11,10 @@ data class DefaultRiskCalculationConfiguration(
     val normalizedTimePerDayToRiskLevelMapping: List<JsonNormalizedTimeToRiskLevelMapping>,
     @SerializedName("normalizedTimePerEWToRiskLevelMapping")
     val normalizedTimePerEWToRiskLevelMapping: List<JsonNormalizedTimeToRiskLevelMapping>,
-    @SerializedName("transmissionRiskLevelMultiplier")
-    val transmissionRiskLevelMultiplier: Double,
     @SerializedName("trlEncoding")
     val trlEncoding: JsonTrlEncoding,
     @SerializedName("trlFilters")
-    val trlFilters: List<JsonTrlFilter>
+    val trlFilters: List<JsonTrlFilter>,
+    @SerializedName("transmissionRiskValueMapping")
+    val transmissionRiskValueMapping: List<JsonTransmissionRiskValueMapping>
 )
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTransmissionRiskValueMapping.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTransmissionRiskValueMapping.kt
new file mode 100644
index 000000000..64bce14b9
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTransmissionRiskValueMapping.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonTransmissionRiskValueMapping(
+    @SerializedName("transmissionRiskLevel")
+    val transmissionRiskLevel: Int,
+    @SerializedName("transmissionRiskValue")
+    val transmissionRiskValue: Double
+)
diff --git a/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json b/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json
index 2b1142843..0e47872a6 100644
--- a/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json
+++ b/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json
@@ -1,28 +1,8 @@
 {
   "__comment__": "JSON has been generated from YAML, see README",
   "defaultRiskCalculationConfiguration": {
-    "minutesAtAttenuationFilters": [
-      {
-        "attenuationRange": {
-          "min": 0,
-          "max": 73,
-          "maxExclusive": true
-        },
-        "dropIfMinutesInRange": {
-          "min": 0,
-          "max": 10,
-          "maxExclusive": true
-        }
-      }
-    ],
-    "trlFilters": [
-      {
-        "dropIfTrlInRange": {
-          "min": 1,
-          "max": 2
-        }
-      }
-    ],
+    "minutesAtAttenuationFilters": [],
+    "trlFilters": [],
     "minutesAtAttenuationWeights": [
       {
         "attenuationRange": {
@@ -39,12 +19,20 @@
           "maxExclusive": true
         },
         "weight": 0.5
+      },
+      {
+        "attenuationRange": {
+          "min": 63,
+          "max": 73,
+          "maxExclusive": true
+        },
+        "weight": 0.3
       }
     ],
     "normalizedTimePerEWToRiskLevelMapping": [
       {
         "normalizedTimeRange": {
-          "min": 0,
+          "min": 5,
           "max": 15,
           "maxExclusive": true
         },
@@ -61,7 +49,7 @@
     "normalizedTimePerDayToRiskLevelMapping": [
       {
         "normalizedTimeRange": {
-          "min": 0,
+          "min": 5,
           "max": 15,
           "maxExclusive": true
         },
@@ -83,11 +71,44 @@
       "reportTypeOffsetConfirmedClinicalDiagnosis": 4,
       "reportTypeOffsetConfirmedTest": 6
     },
-    "transmissionRiskLevelMultiplier": 0.2
+    "transmissionRiskValueMapping": [
+      {
+        "transmissionRiskLevel": 1,
+        "transmissionRiskValue": 0
+      },
+      {
+        "transmissionRiskLevel": 2,
+        "transmissionRiskValue": 0
+      },
+      {
+        "transmissionRiskLevel": 3,
+        "transmissionRiskValue": 0.6
+      },
+      {
+        "transmissionRiskLevel": 4,
+        "transmissionRiskValue": 0.8
+      },
+      {
+        "transmissionRiskLevel": 5,
+        "transmissionRiskValue": 1
+      },
+      {
+        "transmissionRiskLevel": 6,
+        "transmissionRiskValue": 1.2
+      },
+      {
+        "transmissionRiskLevel": 7,
+        "transmissionRiskValue": 1.4
+      },
+      {
+        "transmissionRiskLevel": 8,
+        "transmissionRiskValue": 1.6
+      }
+    ]
   },
   "testCases": [
     {
-      "description": "drop Exposure Windows that do not match minutesAtAttenuationFilters (< 10 minutes)",
+      "description": "keep Exposure Windows (< 10 minutes)",
       "exposureWindows": [
         {
           "ageInDays": 1,
@@ -109,15 +130,15 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
-      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
       "expAgeOfMostRecentDateWithHighRisk": null,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
-      "expNumberOfDaysWithLowRisk": 0,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithLowRisk": 1,
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
-      "description": "keep Exposure Windows that match minutesAtAttenuationFilters (>= 10 minutes)",
+      "description": "keep Exposure Windows (>= 10 minutes)",
       "exposureWindows": [
         {
           "ageInDays": 1,
@@ -139,15 +160,15 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
       "expAgeOfMostRecentDateWithLowRisk": 1,
       "expAgeOfMostRecentDateWithHighRisk": null,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expNumberOfDaysWithLowRisk": 1,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
-      "description": "drop Exposure Windows that do not match minutesAtAttenuationFilters (>= 73 dB)",
+      "description": "keep Exposure Windows (>= 73 dB)",
       "exposureWindows": [
         {
           "ageInDays": 1,
@@ -169,15 +190,15 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
       "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": null,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expNumberOfDaysWithLowRisk": 0,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
-      "description": "keep Exposure Windows that match minutesAtAttenuationFilters (< 73 dB)",
+      "description": "keep Exposure Windows (< 73 dB)",
       "exposureWindows": [
         {
           "ageInDays": 1,
@@ -199,15 +220,15 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
-      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": null,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
-      "expNumberOfDaysWithLowRisk": 1,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
-      "description": "drop Exposure Windows that do not match trlFilters (<= 2)",
+      "description": "keep Exposure Windows with TRL <= 2",
       "exposureWindows": [
         {
           "ageInDays": 1,
@@ -229,15 +250,15 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
       "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": null,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expNumberOfDaysWithLowRisk": 0,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
-      "description": "keep Exposure Windows that match trlFilters (> 2)",
+      "description": "keep Exposure Windows with TRL > 2",
       "exposureWindows": [
         {
           "ageInDays": 1,
@@ -259,12 +280,62 @@
         }
       ],
       "expTotalRiskLevel": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expNumberOfDaysWithLowRisk": 1,
+      "expNumberOfDaysWithHighRisk": 0,
       "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "identify Exposure Window as no risk based on normalizedTime (< 5)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 299
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "identify Exposure Window as Low Risk based on normalizedTime (>= 5)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
       "expAgeOfMostRecentDateWithLowRisk": 1,
       "expAgeOfMostRecentDateWithHighRisk": null,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expNumberOfDaysWithLowRisk": 1,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
       "description": "identify Exposure Window as Low Risk based on normalizedTime (< 15)",
@@ -294,12 +365,12 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
       "expAgeOfMostRecentDateWithLowRisk": 1,
       "expAgeOfMostRecentDateWithHighRisk": null,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expNumberOfDaysWithLowRisk": 1,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
       "description": "identify Exposure Window as High Risk based on normalizedTime (>= 15)",
@@ -329,12 +400,55 @@
         }
       ],
       "expTotalRiskLevel": 2,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
       "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": 1,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 1,
       "expNumberOfDaysWithLowRisk": 0,
-      "expNumberOfDaysWithHighRisk": 1
+      "expNumberOfDaysWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 1
+    },
+    {
+      "description": "ignore Exposure Windows with no Risk Level",
+      "exposureWindows": [
+        {
+          "ageInDays": 2,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 299
+            }
+          ]
+        },
+        {
+          "ageInDays": 3,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 3,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expNumberOfDaysWithLowRisk": 1,
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
       "description": "identify the most recent date with Low Risk",
@@ -395,12 +509,12 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 3,
       "expAgeOfMostRecentDateWithLowRisk": 2,
       "expAgeOfMostRecentDateWithHighRisk": null,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expNumberOfDaysWithLowRisk": 3,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 3,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
       "description": "count Exposure Windows with same Date/TRL/CallibrationConfidence only once towards distinct encounters with Low Risk",
@@ -443,12 +557,12 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
       "expAgeOfMostRecentDateWithLowRisk": 1,
       "expAgeOfMostRecentDateWithHighRisk": null,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expNumberOfDaysWithLowRisk": 1,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
       "description": "count Exposure Windows with same Date/TRL but different CallibrationConfidence separately towards distinct encounters with Low Risk",
@@ -491,12 +605,12 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 2,
       "expAgeOfMostRecentDateWithLowRisk": 1,
       "expAgeOfMostRecentDateWithHighRisk": null,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expNumberOfDaysWithLowRisk": 1,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 2,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
       "description": "count Exposure Windows with same Date/CallibrationConfidence but different TRL separately towards distinct encounters with Low Risk",
@@ -539,12 +653,12 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 2,
       "expAgeOfMostRecentDateWithLowRisk": 1,
       "expAgeOfMostRecentDateWithHighRisk": null,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expNumberOfDaysWithLowRisk": 1,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 2,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
       "description": "count Exposure Windows with same TRL/CallibrationConfidence but different Date separately towards distinct encounters with Low Risk",
@@ -587,12 +701,12 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 2,
       "expAgeOfMostRecentDateWithLowRisk": 1,
       "expAgeOfMostRecentDateWithHighRisk": null,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expNumberOfDaysWithLowRisk": 2,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 2,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
       "description": "determine High Risk in total if there are sufficient Exposure Windows with a Low Risk",
@@ -653,12 +767,12 @@
         }
       ],
       "expTotalRiskLevel": 2,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
       "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": 1,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expNumberOfDaysWithLowRisk": 0,
-      "expNumberOfDaysWithHighRisk": 1
+      "expNumberOfDaysWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
       "description": "identify the most recent date with High Risk",
@@ -719,12 +833,12 @@
         }
       ],
       "expTotalRiskLevel": 2,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
       "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": 2,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 3,
       "expNumberOfDaysWithLowRisk": 0,
-      "expNumberOfDaysWithHighRisk": 3
+      "expNumberOfDaysWithHighRisk": 3,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 3
     },
     {
       "description": "count Exposure Windows with same Date/TRL/CallibrationConfidence only once towards distinct encounters with High Risk",
@@ -767,12 +881,12 @@
         }
       ],
       "expTotalRiskLevel": 2,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
       "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": 1,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 1,
       "expNumberOfDaysWithLowRisk": 0,
-      "expNumberOfDaysWithHighRisk": 1
+      "expNumberOfDaysWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 1
     },
     {
       "description": "count Exposure Windows with same Date/TRL but different CallibrationConfidence separately towards distinct encounters with High Risk",
@@ -815,12 +929,12 @@
         }
       ],
       "expTotalRiskLevel": 2,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
       "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": 1,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 2,
       "expNumberOfDaysWithLowRisk": 0,
-      "expNumberOfDaysWithHighRisk": 1
+      "expNumberOfDaysWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 2
     },
     {
       "description": "count Exposure Windows with same Date/CallibrationConfidence but different TRL separately towards distinct encounters with High Risk",
@@ -863,12 +977,12 @@
         }
       ],
       "expTotalRiskLevel": 2,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
       "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": 1,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 2,
       "expNumberOfDaysWithLowRisk": 0,
-      "expNumberOfDaysWithHighRisk": 1
+      "expNumberOfDaysWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 2
     },
     {
       "description": "count Exposure Windows with same TRL/CallibrationConfidence but different Date separately towards distinct encounters with High Risk",
@@ -911,12 +1025,12 @@
         }
       ],
       "expTotalRiskLevel": 2,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
       "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": 1,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 2,
       "expNumberOfDaysWithLowRisk": 0,
-      "expNumberOfDaysWithHighRisk": 2
+      "expNumberOfDaysWithHighRisk": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 2
     },
     {
       "description": "determine High Risk in total if there is at least one Exposure Window with High Risk",
@@ -959,23 +1073,23 @@
         }
       ],
       "expTotalRiskLevel": 2,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
       "expAgeOfMostRecentDateWithLowRisk": 2,
       "expAgeOfMostRecentDateWithHighRisk": 1,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 1,
       "expNumberOfDaysWithLowRisk": 1,
-      "expNumberOfDaysWithHighRisk": 1
+      "expNumberOfDaysWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 1
     },
     {
       "description": "handle empty set of Exposure Windows",
       "exposureWindows": [],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": null,
       "expNumberOfDaysWithLowRisk": 0,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
       "description": "handle empty set of Scan Instances (should never happen)",
@@ -989,12 +1103,12 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": null,
       "expNumberOfDaysWithLowRisk": 0,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
       "description": "handle a typicalAttenuation: of zero (should never happen)",
@@ -1019,12 +1133,12 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
       "expAgeOfMostRecentDateWithLowRisk": 1,
       "expAgeOfMostRecentDateWithHighRisk": null,
       "expNumberOfDaysWithLowRisk": 1,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
       "description": "handle secondsSinceLastScan of zero (should never happen)",
@@ -1054,12 +1168,12 @@
         }
       ],
       "expTotalRiskLevel": 1,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
-      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": null,
-      "expNumberOfDaysWithLowRisk": 1,
-      "expNumberOfDaysWithHighRisk": 0
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 0,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
     },
     {
       "description": "ignores negative secondsSinceLastScan (can happen when time-travelling, not officially supported)",
@@ -1094,12 +1208,12 @@
         }
       ],
       "expTotalRiskLevel": 2,
-      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
-      "expTotalMinimumDistinctEncountersWithHighRisk": 1,
       "expAgeOfMostRecentDateWithLowRisk": null,
       "expAgeOfMostRecentDateWithHighRisk": 1,
       "expNumberOfDaysWithLowRisk": 0,
-      "expNumberOfDaysWithHighRisk": 1
+      "expNumberOfDaysWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 1
     }
   ]
 }
\ No newline at end of file
-- 
GitLab