diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionConsentFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionConsentFragmentTest.kt
index 4b7f88440901b41d38e243161bfd088e54e4ac6e..7ea2e16565672eb475f993f182df41a03fd66880 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionConsentFragmentTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionConsentFragmentTest.kt
@@ -7,7 +7,7 @@ import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator
 import de.rki.coronawarnapp.nearby.modules.tekhistory.TEKHistoryProvider
 import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository
 import de.rki.coronawarnapp.submission.SubmissionRepository
-import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor
+import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor
 import de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentFragment
 import de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentFragmentArgs
 import de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentViewModel
@@ -31,7 +31,7 @@ class SubmissionConsentFragmentTest : BaseUITest() {
     @MockK lateinit var submissionRepository: SubmissionRepository
     @MockK lateinit var interoperabilityRepository: InteroperabilityRepository
     @MockK lateinit var tekHistoryProvider: TEKHistoryProvider
-    @MockK lateinit var qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor
+    @MockK lateinit var testRegistrationStateProcessor: TestRegistrationStateProcessor
     @MockK lateinit var qrCodeValidator: CoronaTestQrCodeValidator
 
     private lateinit var viewModel: SubmissionConsentViewModel
@@ -48,7 +48,7 @@ class SubmissionConsentFragmentTest : BaseUITest() {
             interoperabilityRepository,
             TestDispatcherProvider(),
             tekHistoryProvider,
-            qrCodeRegistrationStateProcessor,
+            testRegistrationStateProcessor,
             submissionRepository,
             qrCodeValidator
         )
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragmentTest.kt
index 68b8a1b80a3d8ae83fad80d050cced8549a2e6b0..1f1b0dac59fa9341c3a57ea7c9c2362955e1b324 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragmentTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragmentTest.kt
@@ -9,9 +9,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
 import dagger.Module
 import dagger.android.ContributesAndroidInjector
 import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.coronatest.TestRegistrationRequest
 import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
-import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor
+import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor
 import io.mockk.MockKAnnotations
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
@@ -36,13 +36,13 @@ class RequestCovidCertificateFragmentTest : BaseUITest() {
 
         every { viewModel.birthDate } returns MutableLiveData(null)
         every { viewModel.registrationState } returns MutableLiveData(
-            QrCodeRegistrationStateProcessor.RegistrationState(ApiRequestState.IDLE)
+            TestRegistrationStateProcessor.State.Idle
         )
 
         setupMockViewModel(
             object : RequestCovidCertificateViewModel.Factory {
                 override fun create(
-                    coronaTestQrCode: CoronaTestQRCode,
+                    testRegistrationRequest: TestRegistrationRequest,
                     coronaTestConsent: Boolean,
                     deleteOldTest: Boolean
                 ): RequestCovidCertificateViewModel = viewModel
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt
index 4eaae484d689e3f167712fb188037a219e6ddaf3..c744a067c8a4c9463485f31d30ddd7b9bacaafcd 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt
@@ -65,16 +65,16 @@ class CoronaTestTestFragment : Fragment(R.layout.fragment_test_coronatest), Auto
             qrcodeScanViewfinder.setCameraPreview(binding.qrcodeScanPreview)
         }
 
-        viewModel.pcrtState.observe2(this) {
-            binding.pcrtData.text = it.getNiceTextForHumans()
+        viewModel.pcrtState.observe2(this) { state ->
+            binding.pcrtData.text = state.getNiceTextForHumans()
         }
         binding.apply {
             pcrtDeleteAction.setOnClickListener { viewModel.deletePCRT() }
             pcrtRefreshAction.setOnClickListener { viewModel.refreshPCRT() }
         }
 
-        viewModel.ratState.observe2(this) {
-            binding.ratData.text = it.getNiceTextForHumans()
+        viewModel.ratState.observe2(this) { state ->
+            binding.ratData.text = state.getNiceTextForHumans()
         }
         binding.apply {
             ratDeleteAction.setOnClickListener { viewModel.deleteRAT() }
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt
index 544158248957931dfca9e3df2390b194c2ff719f..79c8f84ce15a08b94c5843b4461ae055b17d398a 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt
@@ -6,12 +6,8 @@ import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
 import de.rki.coronawarnapp.coronatest.CoronaTestRepository
-import de.rki.coronawarnapp.coronatest.latestPCRT
-import de.rki.coronawarnapp.coronatest.latestRAT
 import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
-import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest
-import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
@@ -28,17 +24,17 @@ class CoronaTestTestFragmentViewModel @AssistedInject constructor(
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
     val errorEvents = SingleLiveEvent<Throwable>()
-    val pcrtState = coronaTestRepository.latestPCRT.map {
-        PCRTState(
-            coronaTest = it
-        )
-    }.asLiveData(context = dispatcherProvider.Default)
+    val pcrtState = coronaTestRepository.coronaTests
+        .map { tests -> tests.filter { it.type == CoronaTest.Type.PCR } }
+        .map { pcrTests ->
+            PCRTState(coronaTests = pcrTests)
+        }.asLiveData(context = dispatcherProvider.Default)
 
-    val ratState = coronaTestRepository.latestRAT.map {
-        RATState(
-            coronaTest = it
-        )
-    }.asLiveData(context = dispatcherProvider.Default)
+    val ratState = coronaTestRepository.coronaTests
+        .map { tests -> tests.filter { it.type == CoronaTest.Type.RAPID_ANTIGEN } }
+        .map { raTests ->
+            RATState(coronaTests = raTests)
+        }.asLiveData(context = dispatcherProvider.Default)
 
     val testsInContactDiary = contactDiaryRepository.testResults.map {
         it.foldIndexed(StringBuilder()) { id, buffer, item ->
@@ -58,12 +54,12 @@ class CoronaTestTestFragmentViewModel @AssistedInject constructor(
 
     fun deletePCRT() = launch {
         try {
-            val pcrTest = coronaTestRepository.latestPCRT.first()
-            if (pcrTest == null) {
-                Timber.d("No PCR test to delete")
-                return@launch
-            }
-            coronaTestRepository.removeTest(pcrTest.identifier)
+            Timber.i("Deleting PCR tests.")
+            coronaTestRepository.coronaTests.first()
+                .filter { it.type == CoronaTest.Type.PCR }
+                .forEach { test ->
+                    coronaTestRepository.removeTest(test.identifier)
+                }
         } catch (e: Exception) {
             Timber.e(e, "Failed to delete PCR test.")
             errorEvents.postValue(e)
@@ -80,14 +76,14 @@ class CoronaTestTestFragmentViewModel @AssistedInject constructor(
         }
     }
 
-    fun deleteRAT() = launch {
+    fun deleteRAT(): Unit = launch {
         try {
-            val raTest = coronaTestRepository.latestRAT.first()
-            if (raTest == null) {
-                Timber.d("No RA test to delete")
-                return@launch
-            }
-            coronaTestRepository.removeTest(raTest.identifier)
+            Timber.i("Deleting RA tests.")
+            coronaTestRepository.coronaTests.first()
+                .filter { it.type == CoronaTest.Type.RAPID_ANTIGEN }
+                .forEach { test ->
+                    coronaTestRepository.removeTest(test.identifier)
+                }
         } catch (e: Exception) {
             Timber.e(e, "Failed to delete RA test.")
             errorEvents.postValue(e)
@@ -105,28 +101,34 @@ class CoronaTestTestFragmentViewModel @AssistedInject constructor(
     }
 
     data class PCRTState(
-        val coronaTest: PCRCoronaTest?
+        val coronaTests: Collection<CoronaTest>
     ) {
         fun getNiceTextForHumans(): String {
-            return coronaTest
-                ?.toString()
-                ?.replace("PCRCoronaTest(", "")
-                ?.replace(",", ",\n")
-                ?.trimEnd { it == ')' }
-                ?: "No PCR test registered."
+            if (coronaTests.isEmpty()) {
+                return "No PCR test registered."
+            }
+            return coronaTests.joinToString("\n") { test ->
+                test.toString()
+                    .replace("PCRCoronaTest(", "")
+                    .replace(",", ",\n")
+                    .trimEnd { it == ')' }
+            }
         }
     }
 
     data class RATState(
-        val coronaTest: RACoronaTest?
+        val coronaTests: Collection<CoronaTest>
     ) {
         fun getNiceTextForHumans(): String {
-            return coronaTest
-                ?.toString()
-                ?.replace("RACoronaTest(", "")
-                ?.replace(",", ",\n")
-                ?.trimEnd { it == ')' }
-                ?: "No rapid antigen test registered."
+            if (coronaTests.isEmpty()) {
+                return "No rapid antigen test registered."
+            }
+            return coronaTests.joinToString("\n") { test ->
+                test.toString()
+                    .replace("RACoronaTest(", "")
+                    .replace(",", ",\n")
+                    .trimEnd { it == ')' }
+            }
         }
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt
index 9af108afeaf5050149af78eed0d289005709cea8..4919100b5ad5edf116f4e680d7ec58cf1de25039 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.coronatest
 
 import de.rki.coronawarnapp.bugreporting.reportProblem
 import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
+import de.rki.coronawarnapp.coronatest.errors.AlreadyRedeemedException
 import de.rki.coronawarnapp.coronatest.errors.CoronaTestNotFoundException
 import de.rki.coronawarnapp.coronatest.errors.DuplicateCoronaTestException
 import de.rki.coronawarnapp.coronatest.migration.PCRTestMigration
@@ -10,6 +11,8 @@ import de.rki.coronawarnapp.coronatest.storage.CoronaTestStorage
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.coronatest.type.CoronaTestProcessor
 import de.rki.coronawarnapp.coronatest.type.TestIdentifier
+import de.rki.coronawarnapp.exception.ExceptionCategory
+import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.util.coroutine.AppScope
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.flow.HotDataFlow
@@ -72,22 +75,65 @@ class CoronaTestRepository @Inject constructor(
 
     private fun getProcessor(type: CoronaTest.Type) = processors.single { it.type == type }
 
-    suspend fun registerTest(request: TestRegistrationRequest): CoronaTest {
-        Timber.tag(TAG).i("registerTest(request=%s)", request)
+    /**
+     * Default preconditions prevent duplicate test registration,
+     * and registration of an already redeemed test.
+     * If pre and post-condition are not met an [IllegalStateException] is thrown.
+     *
+     * @return the new test that was registered (or an exception is thrown)
+     */
+    suspend fun registerTest(
+        request: TestRegistrationRequest,
+        preCondition: ((Collection<CoronaTest>) -> Boolean) = { currentTests ->
+            if (currentTests.any { it.type == request.type }) {
+                throw DuplicateCoronaTestException("There is already a test of this type: ${request.type}.")
+            }
+            true
+        },
+        postCondition: ((CoronaTest) -> Boolean) = { newTest ->
+            if (newTest.isRedeemed) {
+                Timber.w("Replacement test was already redeemed, removing it, will not use.")
+                throw AlreadyRedeemedException(newTest)
+            }
+            true
+        }
+    ): CoronaTest {
+        Timber.tag(TAG).i(
+            "registerTest(request=%s, preCondition=%s, postCondition=%s)",
+            request, preCondition, postCondition
+        )
 
         // We check early, if there is no processor, crash early, "should" never happen though...
         val processor = getProcessor(request.type)
 
         val currentTests = internalData.updateBlocking {
-            if (values.any { it.type == request.type }) {
-                throw DuplicateCoronaTestException("There is already a test of this type: ${request.type}.")
+            if (!preCondition(values)) {
+                throw IllegalStateException("PreCondition for current tests not fullfilled.")
             }
 
-            val test = processor.create(request).also {
+            val existing = values.singleOrNull { it.type == request.type }
+
+            val newTest = processor.create(request).also {
                 Timber.tag(TAG).i("New test created: %s", it)
             }
 
-            toMutableMap().apply { this[test.identifier] = test }
+            if (!postCondition(newTest)) {
+                throw IllegalStateException("PostCondition for new tests not fullfilled.")
+            }
+
+            if (existing != null) {
+                Timber.tag(TAG).w("We already have a test of this type, removing old test: %s", request)
+                try {
+                    getProcessor(existing.type).onRemove(existing)
+                } catch (e: Exception) {
+                    e.report(ExceptionCategory.INTERNAL)
+                }
+            }
+
+            toMutableMap().apply {
+                existing?.let { remove(it.identifier) }
+                this[newTest.identifier] = newTest
+            }
         }
 
         return currentTests[request.identifier]!!
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestRegistrationRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestRegistrationRequest.kt
index 99ab06763c99b475ee7f25a631f09bac318dac55..5e0ec73e6dbf2d1dda8d64bc83c15bf05faf8bca 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestRegistrationRequest.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestRegistrationRequest.kt
@@ -1,9 +1,10 @@
 package de.rki.coronawarnapp.coronatest
 
+import android.os.Parcelable
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import org.joda.time.LocalDate
 
-interface TestRegistrationRequest {
+interface TestRegistrationRequest : Parcelable {
     val type: CoronaTest.Type
     val identifier: String
     val isDccSupportedByPoc: Boolean
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/errors/AlreadyRedeemedException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/errors/AlreadyRedeemedException.kt
new file mode 100644
index 0000000000000000000000000000000000000000..40ceeef924d23b21513c4e4ccede47ba3b86cf52
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/errors/AlreadyRedeemedException.kt
@@ -0,0 +1,7 @@
+package de.rki.coronawarnapp.coronatest.errors
+
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
+
+class AlreadyRedeemedException(
+    coronaTest: CoronaTest
+) : IllegalArgumentException("Test was already redeemed ${coronaTest.identifier}")
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt
index 9215b9f36c135c5fa7d96017bd02f82c05932c8c..8d75bf3d4be2413a59a57b76081bdb7152996d8c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.submission
 
 import de.rki.coronawarnapp.coronatest.CoronaTestRepository
 import de.rki.coronawarnapp.coronatest.TestRegistrationRequest
+import de.rki.coronawarnapp.coronatest.errors.AlreadyRedeemedException
 import de.rki.coronawarnapp.coronatest.errors.CoronaTestNotFoundException
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
@@ -20,7 +21,6 @@ import timber.log.Timber
 import javax.inject.Inject
 import javax.inject.Singleton
 
-@Suppress("LongParameterList")
 @Singleton
 class SubmissionRepository @Inject constructor(
     @AppScope private val scope: CoroutineScope,
@@ -92,6 +92,32 @@ class SubmissionRepository @Inject constructor(
         return coronaTest
     }
 
+    /**
+     * Attempt to register a new test, but if it is already redeemed, keep the previous test.
+     */
+    suspend fun tryReplaceTest(request: TestRegistrationRequest): CoronaTest {
+        Timber.tag(TAG).v("tryReplaceTest(request=%s)", request)
+
+        val coronaTest = coronaTestRepository.registerTest(
+            request = request,
+            preCondition = { currentTests ->
+                if (currentTests.any { it.type == request.type }) {
+                    Timber.tag(TAG).i("Test type already exists, will try to replace.")
+                }
+                true
+            },
+            postCondition = { newTest ->
+                if (newTest.isRedeemed) {
+                    Timber.w("Replacement test was already redeemed, removing it, will not use.")
+                    throw AlreadyRedeemedException(newTest)
+                }
+                true
+            }
+        )
+        Timber.d("Registered test %s -> %s", request, coronaTest)
+        return coronaTest
+    }
+
     suspend fun reset() {
         Timber.tag(TAG).v("reset()")
         tekHistoryStorage.clear()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/TestRegistrationStateProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/TestRegistrationStateProcessor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..dd34a6e48cd3a1970ed050989c295124129eba03
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/TestRegistrationStateProcessor.kt
@@ -0,0 +1,104 @@
+package de.rki.coronawarnapp.submission
+
+import android.content.Context
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder
+import de.rki.coronawarnapp.coronatest.TestRegistrationRequest
+import de.rki.coronawarnapp.coronatest.errors.AlreadyRedeemedException
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.exception.ExceptionCategory
+import de.rki.coronawarnapp.exception.http.BadRequestException
+import de.rki.coronawarnapp.exception.http.CwaClientError
+import de.rki.coronawarnapp.exception.http.CwaServerError
+import de.rki.coronawarnapp.exception.http.CwaWebException
+import de.rki.coronawarnapp.exception.reporting.report
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import javax.inject.Inject
+
+class TestRegistrationStateProcessor @Inject constructor(
+    private val submissionRepository: SubmissionRepository
+) {
+
+    private val mutex = Mutex()
+
+    sealed class State {
+        object Idle : State()
+        object Working : State()
+        data class TestRegistered(val test: CoronaTest) : State()
+
+        data class Error(val exception: Exception) : State() {
+            fun getDialogBuilder(context: Context): MaterialAlertDialogBuilder {
+                val builder = MaterialAlertDialogBuilder(context).apply {
+                    setCancelable(true)
+                }
+
+                return when (exception) {
+                    is AlreadyRedeemedException -> builder.apply {
+                        setTitle(R.string.submission_error_dialog_web_tan_redeemed_title)
+                        setMessage(R.string.submission_error_dialog_web_tan_redeemed_body)
+                        setPositiveButton(android.R.string.ok) { _, _ ->
+                            /* dismiss */
+                        }
+                    }
+                    is BadRequestException -> builder.apply {
+                        setTitle(R.string.submission_qr_code_scan_invalid_dialog_headline)
+                        setMessage(R.string.submission_qr_code_scan_invalid_dialog_body)
+                        setPositiveButton(android.R.string.ok) { _, _ ->
+                            /* dismiss */
+                        }
+                    }
+                    is CwaClientError, is CwaServerError -> builder.apply {
+                        setTitle(R.string.submission_error_dialog_web_generic_error_title)
+                        setMessage(R.string.submission_error_dialog_web_generic_network_error_body)
+                        setPositiveButton(android.R.string.ok) { _, _ ->
+                            /* dismiss */
+                        }
+                    }
+                    is CwaWebException -> builder.apply {
+                        setTitle(R.string.submission_error_dialog_web_generic_error_title)
+                        setMessage(R.string.submission_error_dialog_web_generic_error_body)
+                        setPositiveButton(android.R.string.ok) { _, _ ->
+                            /* dismiss */
+                        }
+                    }
+                    else -> exception.toErrorDialogBuilder(context)
+                }
+            }
+        }
+    }
+
+    private val stateInternal = MutableStateFlow<State>(State.Idle)
+    val state: Flow<State> = stateInternal
+
+    suspend fun startRegistration(
+        request: TestRegistrationRequest,
+        isSubmissionConsentGiven: Boolean,
+        allowReplacement: Boolean,
+    ): CoronaTest? = mutex.withLock {
+        return try {
+            stateInternal.value = State.Working
+
+            val coronaTest = if (allowReplacement) {
+                submissionRepository.tryReplaceTest(request)
+            } else {
+                submissionRepository.registerTest(request)
+            }
+
+            if (isSubmissionConsentGiven) {
+                submissionRepository.giveConsentToSubmission(type = coronaTest.type)
+            }
+            stateInternal.value = State.TestRegistered(test = coronaTest)
+            coronaTest
+        } catch (err: Exception) {
+            stateInternal.value = State.Error(exception = err)
+            if (err !is CwaWebException) {
+                err.report(ExceptionCategory.INTERNAL)
+            }
+            null
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragment.kt
index 2635a01a5d5cdcfec0d8b5f48f392d1616777c46..40f21e02a4f5008abcd635abf7e1b24a3281d478 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragment.kt
@@ -1,30 +1,22 @@
 package de.rki.coronawarnapp.ui.submission.covidcertificate
 
 import android.os.Bundle
-import androidx.fragment.app.Fragment
 import android.view.View
+import androidx.activity.addCallback
 import androidx.core.view.isInvisible
 import androidx.core.view.isVisible
 import androidx.core.widget.doOnTextChanged
+import androidx.fragment.app.Fragment
 import androidx.navigation.fragment.findNavController
 import androidx.navigation.fragment.navArgs
 import com.google.android.material.datepicker.MaterialDatePicker
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import de.rki.coronawarnapp.NavGraphDirections
 import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder
-import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode
-import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
-import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.PCR
-import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.RAPID_ANTIGEN
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.databinding.FragmentRequestCovidCertificateBinding
 import de.rki.coronawarnapp.exception.http.BadRequestException
-import de.rki.coronawarnapp.exception.http.CwaClientError
-import de.rki.coronawarnapp.exception.http.CwaServerError
-import de.rki.coronawarnapp.exception.http.CwaWebException
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
-import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor
-import de.rki.coronawarnapp.util.DialogHelper
+import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor.State
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toDayFormat
 import de.rki.coronawarnapp.util.di.AutoInject
 import de.rki.coronawarnapp.util.ui.doNavigate
@@ -33,7 +25,6 @@ import de.rki.coronawarnapp.util.ui.viewBinding
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted
 import org.joda.time.LocalDate
-import timber.log.Timber
 import javax.inject.Inject
 
 class RequestCovidCertificateFragment : Fragment(R.layout.fragment_request_covid_certificate), AutoInject {
@@ -43,7 +34,7 @@ class RequestCovidCertificateFragment : Fragment(R.layout.fragment_request_covid
         factoryProducer = { viewModelFactory },
         constructorCall = { factory, _ ->
             factory as RequestCovidCertificateViewModel.Factory
-            factory.create(args.coronaTestQrCode, args.coronaTestConsent, args.deleteOldTest)
+            factory.create(args.testRegistrationRequest, args.coronaTestConsent, args.deleteOldTest)
         }
     )
     private val binding by viewBinding<FragmentRequestCovidCertificateBinding>()
@@ -51,7 +42,7 @@ class RequestCovidCertificateFragment : Fragment(R.layout.fragment_request_covid
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) =
         with(binding) {
-            val isPCR = args.coronaTestQrCode is CoronaTestQRCode.PCR
+            val isPCR = args.testRegistrationRequest.type == CoronaTest.Type.PCR
             birthDateGroup.isVisible = isPCR
             privacyCard.pcrExtraBullet.isVisible = isPCR
 
@@ -59,7 +50,9 @@ class RequestCovidCertificateFragment : Fragment(R.layout.fragment_request_covid
                 if (text.toString().isEmpty()) viewModel.birthDateChanged(null)
             }
 
+            requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { showCloseDialog() }
             toolbar.setNavigationOnClickListener { showCloseDialog() }
+
             agreeButton.setOnClickListener { viewModel.onAgreeGC() }
             disagreeButton.setOnClickListener { viewModel.onDisagreeGC() }
             dateInputEdit.setOnClickListener { openDatePicker() }
@@ -68,70 +61,60 @@ class RequestCovidCertificateFragment : Fragment(R.layout.fragment_request_covid
             viewModel.events.observe(viewLifecycleOwner) { event ->
                 when (event) {
                     Back -> popBackStack()
-                    ToDispatcherScreen -> doNavigate(
+
+                    ToDispatcherScreen ->
                         RequestCovidCertificateFragmentDirections
                             .actionRequestCovidCertificateFragmentToDispatcherFragment()
-                    )
-                    ToHomeScreen -> doNavigate(
+                            .run { doNavigate(this) }
+
+                    ToHomeScreen ->
                         RequestCovidCertificateFragmentDirections.actionRequestCovidCertificateFragmentToHomeFragment()
-                    )
+                            .run { doNavigate(this) }
                 }
             }
             viewModel.birthDate.observe(viewLifecycleOwner) { date -> agreeButton.isEnabled = !isPCR || date != null }
-            viewModel.registrationError.observe(viewLifecycleOwner) { DialogHelper.showDialog(buildErrorDialog(it)) }
             viewModel.registrationState.observe(viewLifecycleOwner) { state -> handleRegistrationState(state) }
-            viewModel.showRedeemedTokenWarning.observe(viewLifecycleOwner) { DialogHelper.showDialog(redeemDialog()) }
-            viewModel.removalError.observe(viewLifecycleOwner) { it.toErrorDialogBuilder(requireContext()).show() }
         }
 
-    private fun handleRegistrationState(state: QrCodeRegistrationStateProcessor.RegistrationState) {
-        when (state.apiRequestState) {
-            ApiRequestState.STARTED -> binding.apply {
-                progressBar.show()
-                agreeButton.isInvisible = true
-                disagreeButton.isInvisible = true
+    private fun handleRegistrationState(state: State) {
+        val isWorking = state is State.Working
+        binding.apply {
+            if (isWorking) progressBar.show() else progressBar.hide()
+            agreeButton.isInvisible = isWorking
+            disagreeButton.isInvisible = isWorking
+        }
+        when (state) {
+            State.Idle,
+            State.Working -> {
+                // Handled above
             }
-            else -> binding.apply {
-                progressBar.hide()
-                agreeButton.isInvisible = false
-                disagreeButton.isInvisible = false
+            is State.Error -> {
+                state.getDialogBuilder(requireContext()).apply {
+                    if (state.exception is BadRequestException) {
+                        setPositiveButton(R.string.submission_qr_code_scan_invalid_dialog_button_positive) { _, _ ->
+                            viewModel.navigateBack()
+                        }
+                        setNegativeButton(R.string.submission_qr_code_scan_invalid_dialog_button_negative) { _, _ ->
+                            viewModel.navigateToDispatcherScreen()
+                        }
+                        setOnCancelListener { viewModel.navigateToDispatcherScreen() }
+                    } else {
+                        setOnDismissListener { viewModel.navigateToDispatcherScreen() }
+                    }
+                }.show()
             }
-        }
-
-        when (state.test?.testResult) {
-            CoronaTestResult.PCR_POSITIVE ->
-                NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = PCR)
-
-            CoronaTestResult.PCR_OR_RAT_PENDING ->
-                NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = state.test.type)
-
-            CoronaTestResult.PCR_NEGATIVE,
-            CoronaTestResult.PCR_INVALID,
-            CoronaTestResult.PCR_REDEEMED ->
-                NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = PCR)
-
-            CoronaTestResult.RAT_POSITIVE ->
-                NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = RAPID_ANTIGEN)
-
-            CoronaTestResult.RAT_NEGATIVE,
-            CoronaTestResult.RAT_INVALID,
-            CoronaTestResult.RAT_PENDING,
-            CoronaTestResult.RAT_REDEEMED ->
-                NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = RAPID_ANTIGEN)
-            null -> {
-                Timber.w("Successful API request, but test was null?")
-                return
+            is State.TestRegistered -> when {
+                state.test.isPositive ->
+                    NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = state.test.type)
+                        .run { doNavigate(this) }
+
+                else ->
+                    NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = state.test.type)
+                        .run { doNavigate(this) }
             }
-        }.run { doNavigate(this) }
+        }
     }
 
-    private fun redeemDialog(): DialogHelper.DialogInstance = DialogHelper.DialogInstance(
-        requireActivity(),
-        R.string.submission_error_dialog_web_tan_redeemed_title,
-        R.string.submission_error_dialog_web_tan_redeemed_body,
-        R.string.submission_error_dialog_web_tan_redeemed_button_positive
-    )
-
     private fun showCloseDialog() = MaterialAlertDialogBuilder(requireContext())
         .setTitle(R.string.request_gc_dialog_title)
         .setMessage(R.string.request_gc_dialog_message)
@@ -151,39 +134,4 @@ class RequestCovidCertificateFragment : Fragment(R.layout.fragment_request_covid
             }
         }
         .show(childFragmentManager, "RequestGreenCertificateFragment.MaterialDatePicker")
-
-    private fun buildErrorDialog(exception: CwaWebException): DialogHelper.DialogInstance =
-        when (exception) {
-            is BadRequestException -> createInvalidScanDialog()
-            is CwaClientError, is CwaServerError -> DialogHelper.DialogInstance(
-                requireActivity(),
-                R.string.submission_error_dialog_web_generic_error_title,
-                R.string.submission_error_dialog_web_generic_network_error_body,
-                R.string.submission_error_dialog_web_generic_error_button_positive,
-                null,
-                true,
-                { viewModel.navigateToDispatcherScreen() }
-            )
-            else -> DialogHelper.DialogInstance(
-                requireActivity(),
-                R.string.submission_error_dialog_web_generic_error_title,
-                R.string.submission_error_dialog_web_generic_error_body,
-                R.string.submission_error_dialog_web_generic_error_button_positive,
-                null,
-                true,
-                { viewModel.navigateToDispatcherScreen() }
-            )
-        }
-
-    private fun createInvalidScanDialog() = DialogHelper.DialogInstance(
-        requireActivity(),
-        R.string.submission_qr_code_scan_invalid_dialog_headline,
-        R.string.submission_qr_code_scan_invalid_dialog_body,
-        R.string.submission_qr_code_scan_invalid_dialog_button_positive,
-        R.string.submission_qr_code_scan_invalid_dialog_button_negative,
-        true,
-        { viewModel.navigateBack() },
-        { viewModel.navigateToDispatcherScreen() },
-        { viewModel.navigateToDispatcherScreen() }
-    )
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModel.kt
index ff0e272e71fbba2a1b6de47be1defa317be6f66c..2e99ed97093486051fb3d51dd253caeb715471ec 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModel.kt
@@ -5,33 +5,24 @@ import androidx.lifecycle.MutableLiveData
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
-import de.rki.coronawarnapp.coronatest.CoronaTestRepository
+import de.rki.coronawarnapp.coronatest.TestRegistrationRequest
 import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
-import de.rki.coronawarnapp.submission.SubmissionRepository
-import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor
+import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
-import kotlinx.coroutines.flow.first
 import org.joda.time.LocalDate
-import timber.log.Timber
 
 class RequestCovidCertificateViewModel @AssistedInject constructor(
-    @Assisted private val coronaTestQrCode: CoronaTestQRCode,
+    @Assisted private val testRegistrationRequest: TestRegistrationRequest,
     @Assisted("coronaTestConsent") private val coronaTestConsent: Boolean,
     @Assisted("deleteOldTest") private val deleteOldTest: Boolean,
-    private val qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor,
+    private val registrationStateProcessor: TestRegistrationStateProcessor,
     private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector,
-    private val submissionRepository: SubmissionRepository,
-    private val coronaTestRepository: CoronaTestRepository,
 ) : CWAViewModel() {
 
-    // Test registration LiveData
-    val showRedeemedTokenWarning = qrCodeRegistrationStateProcessor.showRedeemedTokenWarning
-    val registrationState = qrCodeRegistrationStateProcessor.registrationState
-    val registrationError = qrCodeRegistrationStateProcessor.registrationError
-    val removalError = SingleLiveEvent<Throwable>()
+    val registrationState = registrationStateProcessor.state.asLiveData2()
 
     private val birthDateData = MutableLiveData<LocalDate>(null)
     val birthDate: LiveData<LocalDate> = birthDateData
@@ -58,38 +49,28 @@ class RequestCovidCertificateViewModel @AssistedInject constructor(
     }
 
     private fun registerAndMaybeDelete(dccConsent: Boolean) = launch {
-        if (deleteOldTest) removeOldTest()
-        registerWithDccConsent(dccConsent)
-    }
-
-    private suspend fun registerWithDccConsent(dccConsent: Boolean) {
-        val consentedQrCode = when (coronaTestQrCode) {
-            is CoronaTestQRCode.PCR -> coronaTestQrCode.copy(
+        val consentedQrCode = when (testRegistrationRequest) {
+            is CoronaTestQRCode.PCR -> testRegistrationRequest.copy(
                 dateOfBirth = birthDateData.value,
                 isDccConsentGiven = dccConsent
             )
-            is CoronaTestQRCode.RapidAntigen -> coronaTestQrCode.copy(isDccConsentGiven = dccConsent)
+            is CoronaTestQRCode.RapidAntigen -> testRegistrationRequest.copy(isDccConsentGiven = dccConsent)
+            else -> testRegistrationRequest
         }
 
-        qrCodeRegistrationStateProcessor.startQrCodeRegistration(consentedQrCode, coronaTestConsent)
-        if (coronaTestConsent) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(consentedQrCode.type)
-    }
+        registrationStateProcessor.startRegistration(
+            request = consentedQrCode,
+            isSubmissionConsentGiven = coronaTestConsent,
+            allowReplacement = deleteOldTest
+        )
 
-    private suspend fun removeOldTest() {
-        try {
-            submissionRepository.testForType(coronaTestQrCode.type).first()?.let {
-                coronaTestRepository.removeTest(it.identifier)
-            } ?: Timber.e("Test for type ${coronaTestQrCode.type} is not found")
-        } catch (e: Exception) {
-            Timber.d(e, "removeOldTest failed")
-            removalError.postValue(e)
-        }
+        if (coronaTestConsent) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(consentedQrCode.type)
     }
 
     @AssistedFactory
     interface Factory : CWAViewModelFactory<RequestCovidCertificateViewModel> {
         fun create(
-            coronaTestQrCode: CoronaTestQRCode,
+            testRegistrationRequest: TestRegistrationRequest,
             @Assisted("coronaTestConsent") coronaTestConsent: Boolean,
             @Assisted("deleteOldTest") deleteOldTest: Boolean
         ): RequestCovidCertificateViewModel
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragment.kt
index 623f30a36ff938ee643c382133385a2ebe348b8c..f2b01f53a3c88b9ad1c6fb223465843dc8910274 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragment.kt
@@ -6,17 +6,12 @@ import android.view.accessibility.AccessibilityEvent
 import androidx.core.view.isVisible
 import androidx.fragment.app.Fragment
 import androidx.navigation.fragment.navArgs
+import de.rki.coronawarnapp.NavGraphDirections
 import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder
-import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.databinding.FragmentSubmissionDeletionWarningBinding
-import de.rki.coronawarnapp.exception.http.BadRequestException
-import de.rki.coronawarnapp.exception.http.CwaClientError
-import de.rki.coronawarnapp.exception.http.CwaServerError
-import de.rki.coronawarnapp.exception.http.CwaWebException
+import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor.State
 import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
-import de.rki.coronawarnapp.util.DialogHelper
 import de.rki.coronawarnapp.util.di.AutoInject
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.ui.doNavigate
@@ -38,7 +33,7 @@ class SubmissionDeletionWarningFragment : Fragment(R.layout.fragment_submission_
         factoryProducer = { viewModelFactory },
         constructorCall = { factory, _ ->
             factory as SubmissionDeletionWarningViewModel.Factory
-            factory.create(args.coronaTestQrCode, args.coronaTestTan, args.isConsentGiven)
+            factory.create(args.testRegistrationRequest, args.isConsentGiven)
         }
     )
     private val binding: FragmentSubmissionDeletionWarningBinding by viewBinding()
@@ -65,57 +60,38 @@ class SubmissionDeletionWarningFragment : Fragment(R.layout.fragment_submission_
         }
 
         viewModel.registrationState.observe2(this) { state ->
-            binding.submissionQrCodeScanSpinner.isVisible = state.isFetching
-            binding.continueButton.isVisible = !state.isFetching && state.coronaTest == null
-        }
-        viewModel.registrationError.observe2(this) {
-            showErrorDialog(it)
-            doNavigate(
-                SubmissionDeletionWarningFragmentDirections
-                    .actionSubmissionDeletionWarningFragmentToSubmissionDispatcherFragment()
-            )
-        }
+            val isWorking = state is State.Working
+            binding.apply {
+                submissionQrCodeScanSpinner.isVisible = isWorking
+                continueButton.isVisible = !isWorking
+            }
+            when (state) {
+                State.Idle,
+                State.Working -> {
+                    // Handled above
+                }
+                is State.Error -> {
+                    state.getDialogBuilder(requireContext()).show()
+                    SubmissionDeletionWarningFragmentDirections
+                        .actionSubmissionDeletionWarningFragmentToSubmissionDispatcherFragment()
+                        .run { doNavigate(this) }
+                }
+                is State.TestRegistered -> when {
+                    state.test.isPositive ->
+                        NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = state.test.type)
+                            .run { doNavigate(this) }
 
-        viewModel.routeToScreen.observe2(this) {
-            Timber.d("Navigating to %s", it)
-            doNavigate(it)
-        }
-    }
+                    else ->
+                        NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = state.test.type)
+                            .run { doNavigate(this) }
+                }
+            }
 
-    private fun showErrorDialog(exception: Throwable) = when (exception) {
-        is InvalidQRCodeException -> DialogHelper.DialogInstance(
-            context = requireActivity(),
-            title = R.string.submission_error_dialog_web_tan_redeemed_title,
-            message = R.string.submission_error_dialog_web_tan_redeemed_body,
-            cancelable = true,
-            positiveButton = R.string.submission_error_dialog_web_tan_redeemed_button_positive,
-            positiveButtonFunction = { /* dismiss */ },
-        ).run { DialogHelper.showDialog(this) }
-        is BadRequestException -> DialogHelper.DialogInstance(
-            context = requireActivity(),
-            title = R.string.submission_qr_code_scan_invalid_dialog_headline,
-            message = R.string.submission_qr_code_scan_invalid_dialog_body,
-            cancelable = true,
-            positiveButton = R.string.submission_qr_code_scan_invalid_dialog_button_positive,
-            positiveButtonFunction = { /* dismiss */ },
-        ).run { DialogHelper.showDialog(this) }
-        is CwaClientError, is CwaServerError -> DialogHelper.DialogInstance(
-            context = requireActivity(),
-            title = R.string.submission_error_dialog_web_generic_error_title,
-            message = R.string.submission_error_dialog_web_generic_network_error_body,
-            cancelable = true,
-            positiveButton = R.string.submission_error_dialog_web_generic_error_button_positive,
-            positiveButtonFunction = { /* dismiss */ },
-        ).run { DialogHelper.showDialog(this) }
-        is CwaWebException -> DialogHelper.DialogInstance(
-            context = requireActivity(),
-            title = R.string.submission_error_dialog_web_generic_error_title,
-            message = R.string.submission_error_dialog_web_generic_error_body,
-            cancelable = true,
-            positiveButton = R.string.submission_error_dialog_web_generic_error_button_positive,
-            positiveButtonFunction = { /* dismiss */ },
-        ).run { DialogHelper.showDialog(this) }
-        else -> exception.toErrorDialogBuilder(requireContext()).show()
+            viewModel.routeToScreen.observe2(this) {
+                Timber.d("Navigating to %s", it)
+                doNavigate(it)
+            }
+        }
     }
 
     override fun onResume() {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningViewModel.kt
index 5db57987c07498586d0921592400b48e558619e8..7f479939cbfc28be5c9039e69e2abaf997bc222d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningViewModel.kt
@@ -1,153 +1,81 @@
 package de.rki.coronawarnapp.ui.submission.deletionwarning
 
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
 import androidx.navigation.NavDirections
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
-import de.rki.coronawarnapp.coronatest.CoronaTestRepository
-import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode
-import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException
+import de.rki.coronawarnapp.coronatest.TestRegistrationRequest
 import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
-import de.rki.coronawarnapp.submission.SubmissionRepository
+import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
-import kotlinx.coroutines.flow.first
 import timber.log.Timber
 
 class SubmissionDeletionWarningViewModel @AssistedInject constructor(
-    @Assisted private val coronaTestQrCode: CoronaTestQRCode?,
-    @Assisted private val coronaTestQrTan: CoronaTestTAN?,
+    @Assisted private val testRegistrationRequest: TestRegistrationRequest,
     @Assisted private val isConsentGiven: Boolean,
-    private val submissionRepository: SubmissionRepository,
-    private val coronaTestRepository: CoronaTestRepository,
+    private val registrationStateProcessor: TestRegistrationStateProcessor,
 ) : CWAViewModel() {
 
     val routeToScreen = SingleLiveEvent<NavDirections>()
-    private val mutableRegistrationState = MutableLiveData(RegistrationState())
-    val registrationState: LiveData<RegistrationState> = mutableRegistrationState
-    val registrationError = SingleLiveEvent<Throwable>()
+    val registrationState = registrationStateProcessor.state.asLiveData2()
 
-    private fun getRegistrationType(): RegistrationType = if (coronaTestQrCode != null) {
-        RegistrationType.QR
-    } else {
-        RegistrationType.TAN
-    }
-
-    // If there is no qrCode, it must be a TAN, and TANs are always PCR
-    internal fun getTestType(): CoronaTest.Type = coronaTestQrCode?.type ?: CoronaTest.Type.PCR
+    internal fun getTestType(): CoronaTest.Type = testRegistrationRequest.type
 
     fun deleteExistingAndRegisterNewTest() = launch {
-        when {
-            coronaTestQrTan != null -> deleteExistingAndRegisterNewTestWitTAN()
-            else -> deleteExistingAndRegisterNewTestWithQrCode()
-        }
-    }
-
-    private suspend fun deleteExistingAndRegisterNewTestWithQrCode() = try {
-        requireNotNull(coronaTestQrCode) { "QR Code was unavailable" }
-        if (coronaTestQrCode.isDccSupportedByPoc) {
+        if (testRegistrationRequest.isDccSupportedByPoc) {
             SubmissionDeletionWarningFragmentDirections
                 .actionSubmissionDeletionWarningFragmentToRequestCovidCertificateFragment(
-                    coronaTestQrCode = coronaTestQrCode,
+                    testRegistrationRequest = testRegistrationRequest,
                     coronaTestConsent = isConsentGiven,
                     deleteOldTest = true
                 ).run { routeToScreen.postValue(this) }
         } else {
-            removeAndRegisterNew(coronaTestQrCode)
+            removeAndRegisterNew(testRegistrationRequest)
         }
-    } catch (e: Exception) {
-        Timber.e(e, "Error during test registration via QR code")
-        mutableRegistrationState.postValue(RegistrationState(isFetching = false))
-        registrationError.postValue(e)
     }
 
-    private suspend fun removeAndRegisterNew(
-        coronaTestQrCode: CoronaTestQRCode
-    ) {
-        // Remove existing test and wait until that is done
-        submissionRepository.testForType(coronaTestQrCode.type).first()?.let {
-            coronaTestRepository.removeTest(it.identifier)
-        } ?: Timber.w("Test we will replace with QR was already removed?")
-
-        mutableRegistrationState.postValue(RegistrationState(isFetching = true))
-        val coronaTest = submissionRepository.registerTest(coronaTestQrCode)
+    private suspend fun removeAndRegisterNew(request: TestRegistrationRequest) {
+        val newTest = registrationStateProcessor.startRegistration(
+            request = request,
+            isSubmissionConsentGiven = isConsentGiven,
+            allowReplacement = true
+        )
 
-        if (coronaTest.isRedeemed) {
-            Timber.d("New test was already redeemed, removing it again: %s", coronaTest)
-            // This does not wait until the test is removed,
-            // the exception handling should navigate the user to a new screen anyways
-            submissionRepository.removeTestFromDevice(type = coronaTest.type)
-            throw InvalidQRCodeException("Test is already redeemed")
+        if (newTest == null) {
+            Timber.w("Test registration failed.")
+            return
+        } else {
+            Timber.d("Continuing with our new CoronaTest: %s", newTest)
         }
 
-        if (isConsentGiven) submissionRepository.giveConsentToSubmission(type = coronaTestQrCode.type)
-
-        continueWithNewTest(coronaTest)
-        mutableRegistrationState.postValue(RegistrationState(coronaTest = coronaTest))
-    }
-
-    private suspend fun deleteExistingAndRegisterNewTestWitTAN() = try {
-        requireNotNull(coronaTestQrTan) { "TAN was unavailable" }
-
-        submissionRepository.testForType(CoronaTest.Type.PCR).first()?.let {
-            coronaTestRepository.removeTest(it.identifier)
-        } ?: Timber.w("Test we will replace with TAN was already removed?")
-
-        mutableRegistrationState.postValue(RegistrationState(isFetching = true))
-
-        val coronaTest = submissionRepository.registerTest(coronaTestQrTan)
-        continueWithNewTest(coronaTest)
-
-        mutableRegistrationState.postValue(RegistrationState(coronaTest = coronaTest))
-    } catch (e: Exception) {
-        Timber.e(e, "Error during test registration via TAN")
-        mutableRegistrationState.postValue(RegistrationState(isFetching = false))
-        registrationError.postValue(e)
-    }
-
-    fun onCancelButtonClick() {
-        SubmissionDeletionWarningFragmentDirections
-            .actionSubmissionDeletionWarningFragmentToSubmissionConsentFragment()
-            .run { routeToScreen.postValue(this) }
-    }
+        when (request) {
+            is CoronaTestTAN ->
+                SubmissionDeletionWarningFragmentDirections
+                    .actionSubmissionDeletionFragmentToSubmissionTestResultNoConsentFragment(newTest.type)
 
-    private fun continueWithNewTest(coronaTest: CoronaTest) {
-        Timber.d("Continuing with our new CoronaTest: %s", coronaTest)
-        val testType = coronaTestQrCode!!.type
-        when (getRegistrationType()) {
-            RegistrationType.QR -> if (coronaTest.isPositive) {
+            else -> if (newTest.isPositive) {
                 SubmissionDeletionWarningFragmentDirections
-                    .actionSubmissionDeletionWarningFragmentToSubmissionTestResultAvailableFragment(testType)
+                    .actionSubmissionDeletionWarningFragmentToSubmissionTestResultAvailableFragment(newTest.type)
             } else {
                 SubmissionDeletionWarningFragmentDirections
-                    .actionSubmissionDeletionWarningFragmentToSubmissionTestResultPendingFragment(testType)
+                    .actionSubmissionDeletionWarningFragmentToSubmissionTestResultPendingFragment(newTest.type)
             }
-
-            RegistrationType.TAN ->
-                SubmissionDeletionWarningFragmentDirections
-                    .actionSubmissionDeletionFragmentToSubmissionTestResultNoConsentFragment(getTestType())
         }.run { routeToScreen.postValue(this) }
     }
 
-    data class RegistrationState(
-        val isFetching: Boolean = false,
-        val coronaTest: CoronaTest? = null,
-    )
-
-    sealed class RegistrationType {
-        object TAN : RegistrationType()
-        object QR : RegistrationType()
+    fun onCancelButtonClick() {
+        SubmissionDeletionWarningFragmentDirections
+            .actionSubmissionDeletionWarningFragmentToSubmissionConsentFragment()
+            .run { routeToScreen.postValue(this) }
     }
 
     @AssistedFactory
     interface Factory : CWAViewModelFactory<SubmissionDeletionWarningViewModel> {
         fun create(
-            coronaTestQrCode: CoronaTestQRCode?,
-            coronaTestTan: CoronaTestTAN?,
+            testRegistrationRequest: TestRegistrationRequest,
             isConsentGiven: Boolean
         ): SubmissionDeletionWarningViewModel
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/QrCodeRegistrationStateProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/QrCodeRegistrationStateProcessor.kt
deleted file mode 100644
index 19ec160f5228e0ec0336abb76c5afdf7f7c9a180..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/QrCodeRegistrationStateProcessor.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package de.rki.coronawarnapp.ui.submission.qrcode
-
-import androidx.lifecycle.MutableLiveData
-import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode
-import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException
-import de.rki.coronawarnapp.coronatest.type.CoronaTest
-import de.rki.coronawarnapp.exception.ExceptionCategory
-import de.rki.coronawarnapp.exception.http.CwaWebException
-import de.rki.coronawarnapp.exception.reporting.report
-import de.rki.coronawarnapp.submission.SubmissionRepository
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
-import de.rki.coronawarnapp.util.ui.SingleLiveEvent
-import timber.log.Timber
-import javax.inject.Inject
-
-class QrCodeRegistrationStateProcessor @Inject constructor(
-    private val submissionRepository: SubmissionRepository
-) {
-
-    data class RegistrationState(
-        val apiRequestState: ApiRequestState,
-        val test: CoronaTest? = null
-    )
-
-    val showRedeemedTokenWarning = SingleLiveEvent<Unit>()
-    val registrationState = MutableLiveData(RegistrationState(ApiRequestState.IDLE))
-    val registrationError = SingleLiveEvent<CwaWebException>()
-
-    suspend fun startQrCodeRegistration(coronaTestQRCode: CoronaTestQRCode, isConsentGiven: Boolean) =
-        try {
-            registrationState.postValue(RegistrationState(ApiRequestState.STARTED))
-            val coronaTest = submissionRepository.registerTest(coronaTestQRCode)
-            if (isConsentGiven) {
-                submissionRepository.giveConsentToSubmission(type = coronaTestQRCode.type)
-            }
-            checkTestResult(coronaTestQRCode, coronaTest)
-            registrationState.postValue(RegistrationState(ApiRequestState.SUCCESS, coronaTest))
-        } catch (err: CwaWebException) {
-            registrationState.postValue(RegistrationState(ApiRequestState.FAILED))
-            registrationError.postValue(err)
-        } catch (err: InvalidQRCodeException) {
-            registrationState.postValue(RegistrationState(ApiRequestState.FAILED))
-            Timber.d("deregisterTestFromDevice()")
-            submissionRepository.removeTestFromDevice(type = coronaTestQRCode.type)
-            showRedeemedTokenWarning.postValue(Unit)
-        } catch (err: Exception) {
-            registrationState.postValue(RegistrationState(ApiRequestState.FAILED))
-            err.report(ExceptionCategory.INTERNAL)
-        }
-
-    private fun checkTestResult(request: CoronaTestQRCode, test: CoronaTest) {
-        if (test.isRedeemed) {
-            throw InvalidQRCodeException("CoronaTestResult already redeemed ${request.registrationIdentifier}")
-        }
-    }
-
-    enum class ValidationState {
-        STARTED, INVALID, SUCCESS
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentFragment.kt
index e5fd022f5390e06bdbdb6a4a1c91e76dcb3429e4..3423316e4783ec40651d668214bef499ba4880cc 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentFragment.kt
@@ -10,14 +10,8 @@ import androidx.fragment.app.Fragment
 import androidx.navigation.fragment.navArgs
 import de.rki.coronawarnapp.NavGraphDirections
 import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.databinding.FragmentSubmissionConsentBinding
-import de.rki.coronawarnapp.exception.http.BadRequestException
-import de.rki.coronawarnapp.exception.http.CwaClientError
-import de.rki.coronawarnapp.exception.http.CwaServerError
-import de.rki.coronawarnapp.exception.http.CwaWebException
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
-import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor.ValidationState
+import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor.State
 import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
 import de.rki.coronawarnapp.util.DialogHelper
 import de.rki.coronawarnapp.util.di.AutoInject
@@ -65,8 +59,8 @@ class SubmissionConsentFragment : Fragment(R.layout.fragment_submission_consent)
                     doNavigate(
                         NavGraphDirections
                             .actionToSubmissionDeletionWarningFragment(
-                                it.consentGiven,
-                                it.coronaTestQRCode
+                                testRegistrationRequest = it.coronaTestQRCode,
+                                isConsentGiven = it.consentGiven,
                             )
                     )
                 }
@@ -76,56 +70,36 @@ class SubmissionConsentFragment : Fragment(R.layout.fragment_submission_consent)
             binding.countries = it
         }
 
-        viewModel.showRedeemedTokenWarning.observe2(this) {
-            val dialog = DialogHelper.DialogInstance(
-                requireActivity(),
-                R.string.submission_error_dialog_web_tan_redeemed_title,
-                R.string.submission_error_dialog_web_tan_redeemed_body,
-                R.string.submission_error_dialog_web_tan_redeemed_button_positive
-            )
-
-            DialogHelper.showDialog(dialog)
-            popBackStack()
-        }
-
-        viewModel.qrCodeValidationState.observe2(this) {
-            if (ValidationState.INVALID == it) {
-                showInvalidQrCodeDialog()
-            }
+        viewModel.qrCodeError.observe2(this) {
+            showInvalidQrCodeDialog()
         }
 
         viewModel.registrationState.observe2(this) { state ->
-            binding.progressSpinner.isVisible = state.apiRequestState == ApiRequestState.STARTED
-            binding.submissionConsentButton.isEnabled = when (state.apiRequestState) {
-                ApiRequestState.STARTED -> false
-                else -> true
+            val isWorking = state is State.Working
+            binding.apply {
+                progressSpinner.isVisible = isWorking
+                submissionConsentButton.isEnabled = !isWorking
             }
-
-            if (ApiRequestState.SUCCESS == state.apiRequestState) {
-                when (state.test?.type) {
-                    CoronaTest.Type.PCR -> throw UnsupportedOperationException()
-                    CoronaTest.Type.RAPID_ANTIGEN -> {
-                        when {
-                            state.test.isPositive ->
-                                doNavigate(
-                                    NavGraphDirections.actionToSubmissionTestResultAvailableFragment(
-                                        CoronaTest.Type.RAPID_ANTIGEN
-                                    )
-                                )
-                            else -> doNavigate(
-                                NavGraphDirections.actionSubmissionTestResultPendingFragment(
-                                    testType = CoronaTest.Type.RAPID_ANTIGEN
-                                )
-                            )
-                        }
-                    }
+            when (state) {
+                State.Idle,
+                State.Working -> {
+                    // Handled above
+                }
+                is State.Error -> {
+                    state.getDialogBuilder(requireContext()).show()
+                    popBackStack()
+                }
+                is State.TestRegistered -> when {
+                    state.test.isPositive ->
+                        NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = state.test.type)
+                            .run { doNavigate(this) }
+
+                    else ->
+                        NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = state.test.type)
+                            .run { doNavigate(this) }
                 }
             }
         }
-
-        viewModel.registrationError.observe2(this) {
-            DialogHelper.showDialog(buildErrorDialog(it))
-        }
     }
 
     override fun onResume() {
@@ -149,49 +123,15 @@ class SubmissionConsentFragment : Fragment(R.layout.fragment_submission_consent)
             R.string.submission_qr_code_scan_invalid_dialog_button_negative,
             true,
             positiveButtonFunction = {},
-            negativeButtonFunction = ::navigateHome
+            negativeButtonFunction = {
+                popBackStack()
+                Unit
+            }
         )
 
         DialogHelper.showDialog(invalidScanDialogInstance)
     }
 
-    private fun buildErrorDialog(exception: CwaWebException): DialogHelper.DialogInstance {
-        return when (exception) {
-            is BadRequestException -> DialogHelper.DialogInstance(
-                requireActivity(),
-                R.string.submission_qr_code_scan_invalid_dialog_headline,
-                R.string.submission_qr_code_scan_invalid_dialog_body,
-                R.string.submission_qr_code_scan_invalid_dialog_button_positive,
-                R.string.submission_qr_code_scan_invalid_dialog_button_negative,
-                true,
-                { },
-                ::navigateHome
-            )
-            is CwaClientError, is CwaServerError -> DialogHelper.DialogInstance(
-                requireActivity(),
-                R.string.submission_error_dialog_web_generic_error_title,
-                R.string.submission_error_dialog_web_generic_network_error_body,
-                R.string.submission_error_dialog_web_generic_error_button_positive,
-                null,
-                true,
-                ::navigateHome
-            )
-            else -> DialogHelper.DialogInstance(
-                requireActivity(),
-                R.string.submission_error_dialog_web_generic_error_title,
-                R.string.submission_error_dialog_web_generic_error_body,
-                R.string.submission_error_dialog_web_generic_error_button_positive,
-                null,
-                true,
-                ::navigateHome
-            )
-        }
-    }
-
-    private fun navigateHome() {
-        popBackStack()
-    }
-
     companion object {
         private const val REQUEST_USER_RESOLUTION = 3000
         fun canHandle(rootUri: String): Boolean = rootUri.startsWith("https://s.coronawarn.app")
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModel.kt
index f6876b62aeebd0f5662569e1a26073a0fd7e8e66..4bd68d5045e1c50daba3de2ac99f2ab086d84fdd 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModel.kt
@@ -9,7 +9,7 @@ import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException
 import de.rki.coronawarnapp.nearby.modules.tekhistory.TEKHistoryProvider
 import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository
 import de.rki.coronawarnapp.submission.SubmissionRepository
-import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor
+import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor
 import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
@@ -22,17 +22,14 @@ class SubmissionConsentViewModel @AssistedInject constructor(
     interoperabilityRepository: InteroperabilityRepository,
     dispatcherProvider: DispatcherProvider,
     private val tekHistoryProvider: TEKHistoryProvider,
-    private val qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor,
+    private val registrationStateProcessor: TestRegistrationStateProcessor,
     private val submissionRepository: SubmissionRepository,
     private val qrCodeValidator: CoronaTestQrCodeValidator
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
     val routeToScreen = SingleLiveEvent<SubmissionNavigationEvents>()
-    val qrCodeValidationState = SingleLiveEvent<QrCodeRegistrationStateProcessor.ValidationState>()
-
-    val showRedeemedTokenWarning = qrCodeRegistrationStateProcessor.showRedeemedTokenWarning
-    val registrationState = qrCodeRegistrationStateProcessor.registrationState
-    val registrationError = qrCodeRegistrationStateProcessor.registrationError
+    val qrCodeError = SingleLiveEvent<Exception>()
+    val registrationState = registrationStateProcessor.state.asLiveData2()
 
     val countries = interoperabilityRepository.countryList
         .asLiveData(context = dispatcherProvider.Default)
@@ -76,24 +73,27 @@ class SubmissionConsentViewModel @AssistedInject constructor(
     }
 
     private suspend fun validateAndRegister(qrCodeString: String) {
-        try {
-            val coronaTestQRCode = qrCodeValidator.validate(qrCodeString)
-            qrCodeValidationState.postValue(QrCodeRegistrationStateProcessor.ValidationState.SUCCESS)
-            val coronaTest = submissionRepository.testForType(coronaTestQRCode.type).first()
-
-            if (coronaTest != null) {
-                routeToScreen.postValue(
-                    SubmissionNavigationEvents.NavigateToDeletionWarningFragmentFromQrCode(
-                        coronaTestQRCode,
-                        consentGiven = true
-                    )
-                )
-            } else {
-                qrCodeRegistrationStateProcessor.startQrCodeRegistration(coronaTestQRCode, true)
-            }
+        val coronaTestQRCode = try {
+            qrCodeValidator.validate(qrCodeString)
         } catch (err: InvalidQRCodeException) {
-            Timber.i(err)
-            qrCodeValidationState.postValue(QrCodeRegistrationStateProcessor.ValidationState.INVALID)
+            Timber.i(err, "Failed to validate QRCode")
+            qrCodeError.postValue(err)
+            return
+        }
+
+        val coronaTest = submissionRepository.testForType(coronaTestQRCode.type).first()
+
+        if (coronaTest != null) {
+            SubmissionNavigationEvents.NavigateToDeletionWarningFragmentFromQrCode(
+                coronaTestQRCode,
+                consentGiven = true
+            ).run { routeToScreen.postValue(this) }
+        } else {
+            registrationStateProcessor.startRegistration(
+                request = coronaTestQRCode,
+                isSubmissionConsentGiven = true,
+                allowReplacement = false
+            )
         }
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt
index 6839584b08883c9d03fcb3e5b107c8123a8bf331..6484865f7cb2706c9559ad71214401bec7d2b5fc 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt
@@ -7,19 +7,15 @@ import android.view.View
 import android.view.accessibility.AccessibilityEvent
 import androidx.fragment.app.Fragment
 import androidx.navigation.fragment.navArgs
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.google.zxing.BarcodeFormat
 import com.journeyapps.barcodescanner.DefaultDecoderFactory
 import de.rki.coronawarnapp.NavGraphDirections
 import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
-import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type
+import de.rki.coronawarnapp.coronatest.errors.AlreadyRedeemedException
 import de.rki.coronawarnapp.databinding.FragmentSubmissionQrCodeScanBinding
 import de.rki.coronawarnapp.exception.http.BadRequestException
-import de.rki.coronawarnapp.exception.http.CwaClientError
-import de.rki.coronawarnapp.exception.http.CwaServerError
-import de.rki.coronawarnapp.exception.http.CwaWebException
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
-import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor
+import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor.State
 import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
 import de.rki.coronawarnapp.util.DialogHelper
 import de.rki.coronawarnapp.util.di.AutoInject
@@ -30,7 +26,6 @@ import de.rki.coronawarnapp.util.ui.popBackStack
 import de.rki.coronawarnapp.util.ui.viewBinding
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted
-import timber.log.Timber
 import javax.inject.Inject
 
 /**
@@ -72,7 +67,10 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co
             when (it) {
                 is SubmissionNavigationEvents.NavigateToDeletionWarningFragmentFromQrCode -> {
                     NavGraphDirections
-                        .actionToSubmissionDeletionWarningFragment(it.consentGiven, it.coronaTestQRCode)
+                        .actionToSubmissionDeletionWarningFragment(
+                            testRegistrationRequest = it.coronaTestQRCode,
+                            isConsentGiven = it.consentGiven,
+                        )
                         .run { doNavigate(this) }
                 }
                 is SubmissionNavigationEvents.NavigateToDispatcher -> navigateToDispatchScreen()
@@ -83,57 +81,44 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co
             }
         }
 
-        viewModel.qrCodeValidationState.observe2(this) {
-            if (QrCodeRegistrationStateProcessor.ValidationState.INVALID == it) {
-                DialogHelper.showDialog(createInvalidScanDialog())
-            }
-        }
-
-        viewModel.registrationError.observe2(this) {
-            DialogHelper.showDialog(buildErrorDialog(it))
-        }
-        viewModel.showRedeemedTokenWarning.observe2(this) {
-            val dialog = DialogHelper.DialogInstance(
-                requireActivity(),
-                R.string.submission_error_dialog_web_tan_redeemed_title,
-                R.string.submission_error_dialog_web_tan_redeemed_body,
-                R.string.submission_error_dialog_web_tan_redeemed_button_positive
-            )
-
-            DialogHelper.showDialog(dialog)
-            goBack()
+        viewModel.qrCodeErrorEvent.observe2(this) {
+            showInvalidQrCodeDialog()
         }
 
         viewModel.registrationState.observe2(this) { state ->
-            when (state.apiRequestState) {
-                ApiRequestState.STARTED -> binding.submissionQrCodeScanSpinner.show()
-                else -> binding.submissionQrCodeScanSpinner.hide()
+            if (state is State.Working) {
+                binding.submissionQrCodeScanSpinner.show()
+            } else {
+                binding.submissionQrCodeScanSpinner.hide()
             }
-            when (state.test?.testResult) {
-                CoronaTestResult.PCR_POSITIVE ->
-                    NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = Type.PCR)
-
-                CoronaTestResult.PCR_OR_RAT_PENDING ->
-                    NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = state.test.type)
-
-                CoronaTestResult.PCR_NEGATIVE,
-                CoronaTestResult.PCR_INVALID,
-                CoronaTestResult.PCR_REDEEMED ->
-                    NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = Type.PCR)
-
-                CoronaTestResult.RAT_POSITIVE ->
-                    NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = Type.RAPID_ANTIGEN)
-
-                CoronaTestResult.RAT_NEGATIVE,
-                CoronaTestResult.RAT_INVALID,
-                CoronaTestResult.RAT_PENDING,
-                CoronaTestResult.RAT_REDEEMED ->
-                    NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = Type.RAPID_ANTIGEN)
-                null -> {
-                    Timber.w("Successful API request, but test was null?")
-                    return@observe2
+            when (state) {
+                State.Idle,
+                State.Working -> {
+                    // Handled above
+                }
+                is State.Error -> {
+                    when (state.exception) {
+                        is BadRequestException -> showInvalidQrCodeDialog()
+                        else -> {
+                            state.getDialogBuilder(requireContext()).apply {
+                                when (state.exception) {
+                                    is AlreadyRedeemedException -> setOnDismissListener { goBack() }
+                                    else -> setOnDismissListener { navigateToDispatchScreen() }
+                                }
+                            }.show()
+                        }
+                    }
                 }
-            }.run { doNavigate(this) }
+                is State.TestRegistered -> when {
+                    state.test.isPositive ->
+                        NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = state.test.type)
+                            .run { doNavigate(this) }
+
+                    else ->
+                        NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = state.test.type)
+                            .run { doNavigate(this) }
+                }
+            }
         }
     }
 
@@ -143,45 +128,23 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co
         }
     }
 
-    private fun buildErrorDialog(exception: CwaWebException): DialogHelper.DialogInstance {
-        return when (exception) {
-            is BadRequestException -> createInvalidScanDialog()
-            is CwaClientError, is CwaServerError -> DialogHelper.DialogInstance(
-                requireActivity(),
-                R.string.submission_error_dialog_web_generic_error_title,
-                R.string.submission_error_dialog_web_generic_network_error_body,
-                R.string.submission_error_dialog_web_generic_error_button_positive,
-                null,
-                true,
-                ::navigateToDispatchScreen
-            )
-            else -> DialogHelper.DialogInstance(
-                requireActivity(),
-                R.string.submission_error_dialog_web_generic_error_title,
-                R.string.submission_error_dialog_web_generic_error_body,
-                R.string.submission_error_dialog_web_generic_error_button_positive,
-                null,
-                true,
-                ::navigateToDispatchScreen
-            )
-        }
-    }
-
     private fun navigateToDispatchScreen() = doNavigate(
         SubmissionQRCodeScanFragmentDirections.actionSubmissionQRCodeScanFragmentToSubmissionDispatcherFragment()
     )
 
-    private fun createInvalidScanDialog() = DialogHelper.DialogInstance(
-        requireActivity(),
-        R.string.submission_qr_code_scan_invalid_dialog_headline,
-        R.string.submission_qr_code_scan_invalid_dialog_body,
-        R.string.submission_qr_code_scan_invalid_dialog_button_positive,
-        R.string.submission_qr_code_scan_invalid_dialog_button_negative,
-        true,
-        { startDecode() },
-        { viewModel.onBackPressed() },
-        { viewModel.onBackPressed() }
-    )
+    private fun showInvalidQrCodeDialog() {
+        MaterialAlertDialogBuilder(requireContext()).apply {
+            setTitle(R.string.submission_qr_code_scan_invalid_dialog_headline)
+            setMessage(R.string.submission_qr_code_scan_invalid_dialog_body)
+            setPositiveButton(R.string.submission_qr_code_scan_invalid_dialog_button_positive) { _, _ ->
+                startDecode()
+            }
+            setNegativeButton(R.string.submission_qr_code_scan_invalid_dialog_button_negative) { _, _ ->
+                viewModel.onBackPressed()
+            }
+            setOnCancelListener { viewModel.onBackPressed() }
+        }.show()
+    }
 
     override fun onRequestPermissionsResult(
         requestCode: Int,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt
index b1e0a834805da9dffe95c759703f5ef3752ce5f9..b707a2e43b34b242a43543bb9e0f685559d7832c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt
@@ -7,7 +7,7 @@ import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator
 import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
 import de.rki.coronawarnapp.submission.SubmissionRepository
-import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor
+import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor
 import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.permission.CameraSettings
@@ -21,21 +21,20 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor(
     dispatcherProvider: DispatcherProvider,
     @Assisted private val isConsentGiven: Boolean,
     private val cameraSettings: CameraSettings,
-    private val qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor,
+    private val registrationStateProcessor: TestRegistrationStateProcessor,
     private val submissionRepository: SubmissionRepository,
     private val qrCodeValidator: CoronaTestQrCodeValidator,
     private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
+
     val events = SingleLiveEvent<SubmissionNavigationEvents>()
-    val qrCodeValidationState = SingleLiveEvent<QrCodeRegistrationStateProcessor.ValidationState>()
-    val showRedeemedTokenWarning = qrCodeRegistrationStateProcessor.showRedeemedTokenWarning
-    val registrationState = qrCodeRegistrationStateProcessor.registrationState
-    val registrationError = qrCodeRegistrationStateProcessor.registrationError
+    val qrCodeErrorEvent = SingleLiveEvent<Exception>()
+    val registrationState = registrationStateProcessor.state.asLiveData2()
 
     fun registerCoronaTest(rawResult: String) = launch {
         try {
             val ctQrCode = qrCodeValidator.validate(rawResult)
-            qrCodeValidationState.postValue(QrCodeRegistrationStateProcessor.ValidationState.SUCCESS)
+
             val coronaTest = submissionRepository.testForType(ctQrCode.type).first()
             when {
                 coronaTest != null -> events.postValue(
@@ -46,7 +45,12 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor(
                 )
 
                 else -> if (!ctQrCode.isDccSupportedByPoc) {
-                    qrCodeRegistrationStateProcessor.startQrCodeRegistration(ctQrCode, isConsentGiven)
+                    registrationStateProcessor.startRegistration(
+                        request = ctQrCode,
+                        isSubmissionConsentGiven = isConsentGiven,
+                        allowReplacement = false
+                    )
+
                     if (isConsentGiven) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(ctQrCode.type)
                 } else {
                     events.postValue(
@@ -56,7 +60,7 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor(
             }
         } catch (err: InvalidQRCodeException) {
             Timber.d(err, "Invalid QrCode")
-            qrCodeValidationState.postValue(QrCodeRegistrationStateProcessor.ValidationState.INVALID)
+            qrCodeErrorEvent.postValue(err)
         }
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanFragment.kt
index 97ea51f53dcb69910aeda363e1097fbcf3f10aff..f669290e7550a91070a4e39dff6d96c0224d0207 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanFragment.kt
@@ -55,8 +55,8 @@ class SubmissionTanFragment : Fragment(R.layout.fragment_submission_tan), AutoIn
                 is SubmissionNavigationEvents.NavigateToDeletionWarningFragmentFromTan ->
                     doNavigate(
                         SubmissionTanFragmentDirections.actionSubmissionTanFragmentToSubmissionDeletionWarningFragment(
+                            testRegistrationRequest = it.coronaTestTan,
                             isConsentGiven = it.consentGiven,
-                            coronaTestTan = it.coronaTestTan
                         )
                     )
             }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt
index eca763ca8350351466411c1cdbf9051ac9d5bde8..d132186293c7d25e07426910920b187be39ec3b6 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.util.viewmodel
 
 import androidx.annotation.CallSuper
 import androidx.lifecycle.ViewModel
+import androidx.lifecycle.asLiveData
 import androidx.lifecycle.viewModelScope
 import de.rki.coronawarnapp.util.coroutine.DefaultDispatcherProvider
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
@@ -27,6 +28,8 @@ abstract class CWAViewModel constructor(
         Timber.tag(tag).v("Initialized")
     }
 
+    fun <T> Flow<T>.asLiveData2() = asLiveData(context = dispatcherProvider.Default)
+
     /**
      * This launches a coroutine on another thread
      * Remember to switch to the main thread if you want to update the UI directly
diff --git a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
index 8d879a4bb77ffea0650d57ebc6590f0d199bfeae..b4f78c8b26e030ee87f1dbbcb67c4ba22d99ece0 100644
--- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
+++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
@@ -715,20 +715,13 @@
         android:name="de.rki.coronawarnapp.ui.submission.deletionwarning.SubmissionDeletionWarningFragment"
         android:label="SubmissionDeletionWarningFragment"
         tools:layout="@layout/fragment_submission_deletion_warning">
+        <argument
+            android:name="testRegistrationRequest"
+            app:argType="de.rki.coronawarnapp.coronatest.TestRegistrationRequest" />
         <argument
             android:name="isConsentGiven"
             android:defaultValue="false"
             app:argType="boolean" />
-        <argument
-            android:name="coronaTestQrCode"
-            android:defaultValue="@null"
-            app:argType="de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode"
-            app:nullable="true" />
-        <argument
-            android:name="coronaTestTan"
-            android:defaultValue="@null"
-            app:argType="de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN"
-            app:nullable="true" />
         <action
             android:id="@+id/action_submissionDeletionWarningFragment_to_submissionDispatcherFragment"
             app:popUpTo="@id/submissionDispatcherFragment"
@@ -834,8 +827,8 @@
         android:label="fragment_request_covid_certificate"
         tools:layout="@layout/fragment_request_covid_certificate">
         <argument
-            android:name="coronaTestQrCode"
-            app:argType="de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode" />
+            android:name="testRegistrationRequest"
+            app:argType="de.rki.coronawarnapp.coronatest.TestRegistrationRequest" />
         <argument
             android:name="coronaTestConsent"
             android:defaultValue="false"
@@ -848,7 +841,8 @@
 
         <action
             android:id="@+id/action_requestCovidCertificateFragment_to_dispatcherFragment"
-            app:popUpTo="@id/submissionDispatcherFragment"
+            app:destination="@id/submissionDispatcherFragment"
+            app:popUpTo="@id/mainFragment"
             app:popUpToInclusive="false" />
 
         <action
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryTest.kt
index 7eb566a58f9810a6458f0c3499b0d2d6243e4a1a..3ec942256f9095bacf839def1c1ed7a44e6b5f64 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryTest.kt
@@ -1,7 +1,9 @@
 package de.rki.coronawarnapp.coronatest
 
 import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
+import de.rki.coronawarnapp.coronatest.errors.DuplicateCoronaTestException
 import de.rki.coronawarnapp.coronatest.migration.PCRTestMigration
+import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
 import de.rki.coronawarnapp.coronatest.storage.CoronaTestStorage
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
@@ -9,6 +11,8 @@ import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest
 import de.rki.coronawarnapp.coronatest.type.pcr.PCRTestProcessor
 import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest
 import de.rki.coronawarnapp.coronatest.type.rapidantigen.RATestProcessor
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.Runs
 import io.mockk.coEvery
@@ -29,20 +33,28 @@ class CoronaTestRepositoryTest : BaseTest() {
     @MockK lateinit var legacyMigration: PCRTestMigration
     @MockK lateinit var contactDiaryRepository: ContactDiaryRepository
 
+    @MockK lateinit var pcrProcessor: PCRTestProcessor
+    @MockK lateinit var raProcessor: RATestProcessor
+
     private var coronaTestsInStorage = mutableSetOf<CoronaTest>()
 
+    private val pcrRegistrationRequest = CoronaTestQRCode.PCR(
+        qrCodeGUID = "pcr-guid"
+    )
     private val pcrTest = PCRCoronaTest(
-        identifier = "pcr-identifier",
+        identifier = pcrRegistrationRequest.identifier,
         lastUpdatedAt = Instant.EPOCH,
         registeredAt = Instant.EPOCH,
         registrationToken = "token",
         testResult = CoronaTestResult.PCR_REDEEMED,
     )
-    @MockK lateinit var pcrProcessor: PCRTestProcessor
 
-    @MockK lateinit var raProcessor: RATestProcessor
+    private val raRegistrationRequest = CoronaTestQRCode.RapidAntigen(
+        hash = "ra-hash",
+        createdAt = Instant.EPOCH
+    )
     private val raTest = RACoronaTest(
-        identifier = "ra-identifier",
+        identifier = raRegistrationRequest.identifier,
         lastUpdatedAt = Instant.EPOCH,
         registeredAt = Instant.EPOCH,
         registrationToken = "token",
@@ -54,9 +66,6 @@ class CoronaTestRepositoryTest : BaseTest() {
     fun setup() {
         MockKAnnotations.init(this)
 
-        coronaTestsInStorage.add(pcrTest)
-        coronaTestsInStorage.add(raTest)
-
         legacyMigration.apply {
             coEvery { startMigration() } returns emptySet()
             coEvery { finishMigration() } just Runs
@@ -75,11 +84,13 @@ class CoronaTestRepositoryTest : BaseTest() {
         }
 
         pcrProcessor.apply {
+            coEvery { create(pcrRegistrationRequest) } returns pcrTest
             coEvery { updateSubmissionConsent(any(), any()) } answers { arg<PCRCoronaTest>(0) }
             every { type } returns CoronaTest.Type.PCR
         }
 
         raProcessor.apply {
+            coEvery { create(raRegistrationRequest) } returns raTest
             coEvery { updateSubmissionConsent(any(), any()) } answers { arg<RACoronaTest>(0) }
             every { type } returns CoronaTest.Type.RAPID_ANTIGEN
         }
@@ -96,8 +107,31 @@ class CoronaTestRepositoryTest : BaseTest() {
 
     @Test
     fun `give submission consent`() = runBlockingTest2(ignoreActive = true) {
+        coronaTestsInStorage.add(pcrTest)
+
         createInstance(this).updateSubmissionConsent(pcrTest.identifier, true)
 
         coVerify { pcrProcessor.updateSubmissionConsent(pcrTest, true) }
     }
+
+    @Test
+    fun `test registration with default conditions`() = runBlockingTest2(ignoreActive = true) {
+        coronaTestsInStorage.clear()
+        val negativePcr = pcrTest.copy(testResult = CoronaTestResult.PCR_NEGATIVE)
+        coEvery { pcrProcessor.create(pcrRegistrationRequest) } returns negativePcr
+        val instance = createInstance(this)
+
+        instance.registerTest(pcrRegistrationRequest) shouldBe negativePcr
+    }
+
+    @Test
+    fun `test registration with default conditions and existing test`() = runBlockingTest2(ignoreActive = true) {
+        coronaTestsInStorage.add(pcrTest)
+
+        val instance = createInstance(this)
+
+        shouldThrow<DuplicateCoronaTestException> {
+            instance.registerTest(pcrRegistrationRequest) shouldBe pcrTest
+        }
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt
index ae77a7e3ca94f391438e9a5f0aedc71cbcc48fe8..1a03d4997566f27f6f93997df101a9bfb352b7a4 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt
@@ -1,15 +1,30 @@
 package de.rki.coronawarnapp.storage
 
 import de.rki.coronawarnapp.coronatest.CoronaTestRepository
+import de.rki.coronawarnapp.coronatest.errors.AlreadyRedeemedException
+import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode
+import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.submission.SubmissionSettings
 import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryStorage
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
 import io.mockk.impl.annotations.MockK
+import io.mockk.slot
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.test.runBlockingTest
+import org.joda.time.Instant
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
+import testhelpers.preferences.mockFlowPreference
 
 class SubmissionRepositoryTest : BaseTest() {
 
@@ -17,9 +32,29 @@ class SubmissionRepositoryTest : BaseTest() {
     @MockK lateinit var tekHistoryStorage: TEKHistoryStorage
     @MockK lateinit var coronaTestRepository: CoronaTestRepository
 
+    private val pcrRegistrationRequest = CoronaTestQRCode.PCR(
+        qrCodeGUID = "pcr-guid"
+    )
+    private val pcrTest = PCRCoronaTest(
+        identifier = pcrRegistrationRequest.identifier,
+        lastUpdatedAt = Instant.EPOCH,
+        registeredAt = Instant.EPOCH,
+        registrationToken = "token",
+        testResult = CoronaTestResult.PCR_REDEEMED,
+    )
+
     @BeforeEach
     fun setUp() {
         MockKAnnotations.init(this)
+
+        coronaTestRepository.apply {
+            every { coronaTests } returns emptyFlow()
+            coEvery { registerTest(pcrRegistrationRequest, any(), any()) } returns pcrTest
+        }
+
+        submissionSettings.apply {
+            every { symptoms } returns mockFlowPreference(null)
+        }
     }
 
     fun createInstance(scope: CoroutineScope) = SubmissionRepository(
@@ -30,7 +65,23 @@ class SubmissionRepositoryTest : BaseTest() {
     )
 
     @Test
-    fun todo() {
-        // TODO
+    fun `tryReplaceTest overrides register test conditions`() = runBlockingTest {
+        val precondition = slot<(Collection<CoronaTest>) -> Boolean>()
+        val postcondition = slot<(CoronaTest) -> Boolean>()
+
+        val instance = createInstance(scope = this)
+
+        instance.tryReplaceTest(pcrRegistrationRequest)
+
+        coVerify { coronaTestRepository.registerTest(any(), capture(precondition), capture(postcondition)) }
+
+        precondition.captured(emptyList()) shouldBe true
+        precondition.captured(listOf(pcrTest)) shouldBe true
+
+        shouldThrow<AlreadyRedeemedException> {
+            postcondition.captured(pcrTest)
+        }
+
+        postcondition.captured(pcrTest.copy(testResult = CoronaTestResult.PCR_NEGATIVE)) shouldBe true
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/TestRegistrationStateProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/TestRegistrationStateProcessorTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..238c38ddeea4605f50275a8b30763e8e7a18e9c7
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/TestRegistrationStateProcessorTest.kt
@@ -0,0 +1,177 @@
+package de.rki.coronawarnapp.submission
+
+import de.rki.coronawarnapp.coronatest.TestRegistrationRequest
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.exception.http.BadRequestException
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class TestRegistrationStateProcessorTest : BaseTest() {
+
+    @MockK lateinit var submissionRepository: SubmissionRepository
+    @MockK lateinit var request: TestRegistrationRequest
+    @MockK lateinit var registeredTest: CoronaTest
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        submissionRepository.apply {
+            coEvery { registerTest(any()) } returns registeredTest
+            coEvery { tryReplaceTest(any()) } returns registeredTest
+            coEvery { giveConsentToSubmission(any()) } just Runs
+        }
+
+        registeredTest.apply {
+            every { type } returns CoronaTest.Type.RAPID_ANTIGEN
+        }
+
+        request.apply {
+            every { type } returns CoronaTest.Type.RAPID_ANTIGEN
+        }
+    }
+
+    private fun createInstance() = TestRegistrationStateProcessor(
+        submissionRepository = submissionRepository
+    )
+
+    @Test
+    fun `register new test - with consent`() = runBlockingTest {
+        val instance = createInstance()
+
+        instance.state.first() shouldBe TestRegistrationStateProcessor.State.Idle
+
+        instance.startRegistration(
+            request = request,
+            isSubmissionConsentGiven = true,
+            allowReplacement = false
+        )
+
+        advanceUntilIdle()
+
+        instance.state.first() shouldBe TestRegistrationStateProcessor.State.TestRegistered(
+            test = registeredTest
+        )
+
+        coVerify {
+            submissionRepository.registerTest(request)
+            submissionRepository.giveConsentToSubmission(CoronaTest.Type.RAPID_ANTIGEN)
+        }
+    }
+
+    @Test
+    fun `register new test - without consent`() = runBlockingTest {
+        val instance = createInstance()
+
+        instance.state.first() shouldBe TestRegistrationStateProcessor.State.Idle
+
+        instance.startRegistration(
+            request = request,
+            isSubmissionConsentGiven = false,
+            allowReplacement = false
+        )
+
+        advanceUntilIdle()
+
+        instance.state.first() shouldBe TestRegistrationStateProcessor.State.TestRegistered(
+            test = registeredTest
+        )
+
+        coVerify {
+            submissionRepository.registerTest(request)
+        }
+        coVerify(exactly = 0) {
+            submissionRepository.giveConsentToSubmission(any())
+        }
+    }
+
+    @Test
+    fun `replace test - with consent`() = runBlockingTest {
+        val instance = createInstance()
+
+        instance.state.first() shouldBe TestRegistrationStateProcessor.State.Idle
+
+        instance.startRegistration(
+            request = request,
+            isSubmissionConsentGiven = true,
+            allowReplacement = true
+        )
+
+        advanceUntilIdle()
+
+        instance.state.first() shouldBe TestRegistrationStateProcessor.State.TestRegistered(
+            test = registeredTest
+        )
+
+        coVerify {
+            submissionRepository.tryReplaceTest(request)
+            submissionRepository.giveConsentToSubmission(CoronaTest.Type.RAPID_ANTIGEN)
+        }
+    }
+
+    @Test
+    fun `replace new test - without consent`() = runBlockingTest {
+        val instance = createInstance()
+
+        instance.state.first() shouldBe TestRegistrationStateProcessor.State.Idle
+
+        instance.startRegistration(
+            request = request,
+            isSubmissionConsentGiven = false,
+            allowReplacement = true
+        )
+
+        advanceUntilIdle()
+
+        instance.state.first() shouldBe TestRegistrationStateProcessor.State.TestRegistered(
+            test = registeredTest
+        )
+
+        coVerify {
+            submissionRepository.tryReplaceTest(request)
+        }
+        coVerify(exactly = 0) {
+            submissionRepository.giveConsentToSubmission(any())
+        }
+    }
+
+    @Test
+    fun `errors are mapped to state`() = runBlockingTest {
+        val instance = createInstance()
+
+        val expectedException = BadRequestException("")
+        coEvery { submissionRepository.registerTest(any()) } throws expectedException
+
+        instance.state.first() shouldBe TestRegistrationStateProcessor.State.Idle
+
+        instance.startRegistration(
+            request = request,
+            isSubmissionConsentGiven = true,
+            allowReplacement = false
+        )
+
+        advanceUntilIdle()
+
+        instance.state.first() shouldBe TestRegistrationStateProcessor.State.Error(
+            exception = expectedException
+        )
+
+        coVerify {
+            submissionRepository.registerTest(request)
+        }
+        coVerify(exactly = 0) {
+            submissionRepository.giveConsentToSubmission(any())
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModelTest.kt
index 009f839c5c79d3bfed61a95225a0f6cea8335c89..7787f4a27c46facb49b5b4b1063951cab25bd446 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModelTest.kt
@@ -1,13 +1,9 @@
 package de.rki.coronawarnapp.ui.submission.covidcertificate
 
-import androidx.lifecycle.MutableLiveData
-import de.rki.coronawarnapp.coronatest.CoronaTestRepository
 import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
-import de.rki.coronawarnapp.submission.SubmissionRepository
-import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor
-import de.rki.coronawarnapp.util.ui.SingleLiveEvent
+import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.Runs
@@ -16,6 +12,7 @@ import io.mockk.coVerify
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import io.mockk.just
+import io.mockk.mockk
 import kotlinx.coroutines.flow.flowOf
 import org.joda.time.Instant
 import org.joda.time.LocalDate
@@ -30,10 +27,8 @@ import testhelpers.extensions.getOrAwaitValue
 @ExtendWith(InstantExecutorExtension::class)
 internal class RequestCovidCertificateViewModelTest : BaseTest() {
 
-    @MockK lateinit var qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor
+    @MockK lateinit var testRegistrationStateProcessor: TestRegistrationStateProcessor
     @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
-    @MockK lateinit var submissionRepository: SubmissionRepository
-    @MockK lateinit var coronaTestRepository: CoronaTestRepository
     @MockK lateinit var coronaTest: CoronaTest
 
     private val date = LocalDate.parse(
@@ -48,24 +43,27 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() {
     fun setup() {
         MockKAnnotations.init(this)
 
-        qrCodeRegistrationStateProcessor.apply {
-            coEvery { startQrCodeRegistration(any(), any()) } just Runs
-            coEvery { registrationError } returns SingleLiveEvent()
-            coEvery { showRedeemedTokenWarning } returns SingleLiveEvent()
-            coEvery { registrationState } returns MutableLiveData()
+        testRegistrationStateProcessor.apply {
+            coEvery { startRegistration(any(), any(), any()) } returns mockk()
+            coEvery { state } returns flowOf(TestRegistrationStateProcessor.State.Idle)
         }
 
-        submissionRepository.apply {
-            coEvery { registerTest(any()) } returns coronaTest
-            coEvery { testForType(any()) } returns flowOf(coronaTest)
-        }
-
-        coEvery { coronaTestRepository.removeTest(any()) } returns coronaTest
-
         every { coronaTest.identifier } returns "identifier"
         every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } just Runs
     }
 
+    private fun createInstance(
+        coronaTestQRCode: CoronaTestQRCode = pcrQRCode,
+        coronTestConsent: Boolean = true,
+        deleteOldTest: Boolean = false
+    ) = RequestCovidCertificateViewModel(
+        testRegistrationRequest = coronaTestQRCode,
+        coronaTestConsent = coronTestConsent,
+        deleteOldTest = deleteOldTest,
+        registrationStateProcessor = testRegistrationStateProcessor,
+        analyticsKeySubmissionCollector = analyticsKeySubmissionCollector
+    )
+
     @Test
     fun birthDateChanged() {
         createInstance().apply {
@@ -81,11 +79,10 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() {
             onAgreeGC()
 
             coVerify {
-                submissionRepository.testForType(any())
-                coronaTestRepository.removeTest(any())
-                qrCodeRegistrationStateProcessor.startQrCodeRegistration(
-                    pcrQRCode.copy(isDccConsentGiven = true, dateOfBirth = date),
-                    any()
+                testRegistrationStateProcessor.startRegistration(
+                    request = pcrQRCode.copy(isDccConsentGiven = true, dateOfBirth = date),
+                    isSubmissionConsentGiven = any(),
+                    allowReplacement = true
                 )
                 analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any())
             }
@@ -99,17 +96,13 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() {
             onAgreeGC()
 
             coVerify {
-                qrCodeRegistrationStateProcessor.startQrCodeRegistration(
-                    pcrQRCode.copy(isDccConsentGiven = true, dateOfBirth = date),
-                    any()
+                testRegistrationStateProcessor.startRegistration(
+                    request = pcrQRCode.copy(isDccConsentGiven = true, dateOfBirth = date),
+                    isSubmissionConsentGiven = any(),
+                    allowReplacement = false
                 )
                 analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any())
             }
-
-            coVerify(exactly = 0) {
-                submissionRepository.testForType(any())
-                coronaTestRepository.removeTest(any())
-            }
         }
     }
 
@@ -119,11 +112,10 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() {
             onDisagreeGC()
 
             coVerify {
-                submissionRepository.testForType(any())
-                coronaTestRepository.removeTest(any())
-                qrCodeRegistrationStateProcessor.startQrCodeRegistration(
-                    pcrQRCode.copy(isDccConsentGiven = false),
-                    any()
+                testRegistrationStateProcessor.startRegistration(
+                    request = pcrQRCode.copy(isDccConsentGiven = false),
+                    isSubmissionConsentGiven = any(),
+                    allowReplacement = true
                 )
                 analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any())
             }
@@ -136,17 +128,13 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() {
             onDisagreeGC()
 
             coVerify {
-                qrCodeRegistrationStateProcessor.startQrCodeRegistration(
-                    pcrQRCode.copy(isDccConsentGiven = false),
-                    any()
+                testRegistrationStateProcessor.startRegistration(
+                    request = pcrQRCode.copy(isDccConsentGiven = false),
+                    isSubmissionConsentGiven = any(),
+                    allowReplacement = false
                 )
                 analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any())
             }
-
-            coVerify(exactly = 0) {
-                submissionRepository.testForType(any())
-                coronaTestRepository.removeTest(any())
-            }
         }
     }
 
@@ -156,11 +144,10 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() {
             onAgreeGC()
 
             coVerify {
-                submissionRepository.testForType(any())
-                coronaTestRepository.removeTest(any())
-                qrCodeRegistrationStateProcessor.startQrCodeRegistration(
-                    ratQRCode.copy(isDccConsentGiven = true, dateOfBirth = date),
-                    any()
+                testRegistrationStateProcessor.startRegistration(
+                    request = ratQRCode.copy(isDccConsentGiven = true, dateOfBirth = date),
+                    isSubmissionConsentGiven = any(),
+                    allowReplacement = true
                 )
                 analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any())
             }
@@ -173,17 +160,13 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() {
             onAgreeGC()
 
             coVerify {
-                qrCodeRegistrationStateProcessor.startQrCodeRegistration(
-                    ratQRCode.copy(isDccConsentGiven = true),
-                    any()
+                testRegistrationStateProcessor.startRegistration(
+                    request = ratQRCode.copy(isDccConsentGiven = true),
+                    isSubmissionConsentGiven = any(),
+                    allowReplacement = false
                 )
                 analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any())
             }
-
-            coVerify(exactly = 0) {
-                submissionRepository.testForType(any())
-                coronaTestRepository.removeTest(any())
-            }
         }
     }
 
@@ -193,11 +176,10 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() {
             onDisagreeGC()
 
             coVerify {
-                submissionRepository.testForType(any())
-                coronaTestRepository.removeTest(any())
-                qrCodeRegistrationStateProcessor.startQrCodeRegistration(
-                    ratQRCode.copy(isDccConsentGiven = false),
-                    any()
+                testRegistrationStateProcessor.startRegistration(
+                    request = ratQRCode.copy(isDccConsentGiven = false),
+                    isSubmissionConsentGiven = any(),
+                    allowReplacement = true
                 )
                 analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any())
             }
@@ -210,17 +192,13 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() {
             onDisagreeGC()
 
             coVerify {
-                qrCodeRegistrationStateProcessor.startQrCodeRegistration(
-                    ratQRCode.copy(isDccConsentGiven = false),
-                    any()
+                testRegistrationStateProcessor.startRegistration(
+                    request = ratQRCode.copy(isDccConsentGiven = false),
+                    isSubmissionConsentGiven = any(),
+                    allowReplacement = false
                 )
                 analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any())
             }
-
-            coVerify(exactly = 0) {
-                submissionRepository.testForType(any())
-                coronaTestRepository.removeTest(any())
-            }
         }
     }
 
@@ -287,18 +265,4 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() {
             }
         }
     }
-
-    private fun createInstance(
-        coronaTestQRCode: CoronaTestQRCode = pcrQRCode,
-        coronTestConsent: Boolean = true,
-        deleteOldTest: Boolean = false
-    ) = RequestCovidCertificateViewModel(
-        coronaTestQrCode = coronaTestQRCode,
-        coronaTestConsent = coronTestConsent,
-        deleteOldTest = deleteOldTest,
-        coronaTestRepository = coronaTestRepository,
-        submissionRepository = submissionRepository,
-        qrCodeRegistrationStateProcessor = qrCodeRegistrationStateProcessor,
-        analyticsKeySubmissionCollector = analyticsKeySubmissionCollector
-    )
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModelTest.kt
index 41eb4280816b12a78fe0c6ba1e830ae58188aeb9..36730890014ffbc629c94692b9501c5c47c2d550 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModelTest.kt
@@ -1,16 +1,13 @@
 package de.rki.coronawarnapp.ui.submission.qrcode.consent
 
-import androidx.lifecycle.MutableLiveData
 import com.google.android.gms.common.api.ApiException
 import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator
 import de.rki.coronawarnapp.nearby.modules.tekhistory.TEKHistoryProvider
 import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository
 import de.rki.coronawarnapp.submission.SubmissionRepository
+import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor
 import de.rki.coronawarnapp.ui.Country
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
-import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor
 import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
-import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.Runs
@@ -21,6 +18,7 @@ import io.mockk.impl.annotations.MockK
 import io.mockk.just
 import io.mockk.mockk
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.extension.ExtendWith
@@ -34,7 +32,7 @@ class SubmissionConsentViewModelTest {
     @MockK lateinit var submissionRepository: SubmissionRepository
     @MockK lateinit var interoperabilityRepository: InteroperabilityRepository
     @MockK lateinit var tekHistoryProvider: TEKHistoryProvider
-    @MockK lateinit var qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor
+    @MockK lateinit var testRegistrationStateProcessor: TestRegistrationStateProcessor
     @MockK lateinit var qrCodeValidator: CoronaTestQrCodeValidator
 
     lateinit var viewModel: SubmissionConsentViewModel
@@ -46,16 +44,17 @@ class SubmissionConsentViewModelTest {
         MockKAnnotations.init(this)
         every { interoperabilityRepository.countryList } returns MutableStateFlow(countryList)
         coEvery { submissionRepository.giveConsentToSubmission(any()) } just Runs
-        coEvery { qrCodeRegistrationStateProcessor.showRedeemedTokenWarning } returns SingleLiveEvent()
-        coEvery { qrCodeRegistrationStateProcessor.registrationState } returns MutableLiveData(
-            QrCodeRegistrationStateProcessor.RegistrationState(ApiRequestState.IDLE)
-        )
-        coEvery { qrCodeRegistrationStateProcessor.registrationError } returns SingleLiveEvent()
+
+        testRegistrationStateProcessor.apply {
+            every { state } returns flowOf(TestRegistrationStateProcessor.State.Idle)
+            coEvery { startRegistration(any(), any(), any()) } returns mockk()
+        }
+
         viewModel = SubmissionConsentViewModel(
             interoperabilityRepository,
             dispatcherProvider = TestDispatcherProvider(),
             tekHistoryProvider,
-            qrCodeRegistrationStateProcessor,
+            testRegistrationStateProcessor,
             submissionRepository,
             qrCodeValidator
         )
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt
index 18b2c8425196c280a168f20a048ac15bb3e5bd9a..977907d7f68634f32c0825e343ba360c85f6be81 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt
@@ -1,17 +1,13 @@
 package de.rki.coronawarnapp.ui.submission.qrcode.scan
 
-import androidx.lifecycle.MutableLiveData
 import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode
 import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator
 import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
 import de.rki.coronawarnapp.submission.SubmissionRepository
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
-import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor
-import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor.ValidationState
+import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor
 import de.rki.coronawarnapp.util.permission.CameraSettings
-import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.Runs
@@ -19,8 +15,10 @@ import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import io.mockk.just
+import io.mockk.mockk
 import io.mockk.verify
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runBlockingTest
 import org.joda.time.Instant
 import org.junit.jupiter.api.BeforeEach
@@ -37,7 +35,7 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() {
     @MockK lateinit var submissionRepository: SubmissionRepository
     @MockK lateinit var cameraSettings: CameraSettings
     @MockK lateinit var qrCodeValidator: CoronaTestQrCodeValidator
-    @MockK lateinit var qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor
+    @MockK lateinit var testRegistrationStateProcessor: TestRegistrationStateProcessor
     @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
 
     @BeforeEach
@@ -45,18 +43,18 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() {
         MockKAnnotations.init(this)
 
         every { submissionRepository.testForType(any()) } returns MutableStateFlow<CoronaTest?>(null)
-        coEvery { qrCodeRegistrationStateProcessor.showRedeemedTokenWarning } returns SingleLiveEvent()
-        coEvery { qrCodeRegistrationStateProcessor.registrationState } returns MutableLiveData(
-            QrCodeRegistrationStateProcessor.RegistrationState(ApiRequestState.IDLE)
-        )
-        coEvery { qrCodeRegistrationStateProcessor.registrationError } returns SingleLiveEvent()
+
+        testRegistrationStateProcessor.apply {
+            every { state } returns flowOf(TestRegistrationStateProcessor.State.Idle)
+            coEvery { startRegistration(any(), any(), any()) } returns mockk()
+        }
     }
 
     private fun createViewModel() = SubmissionQRCodeScanViewModel(
         isConsentGiven = true,
         dispatcherProvider = TestDispatcherProvider(),
         cameraSettings = cameraSettings,
-        qrCodeRegistrationStateProcessor = qrCodeRegistrationStateProcessor,
+        registrationStateProcessor = testRegistrationStateProcessor,
         submissionRepository = submissionRepository,
         qrCodeValidator = qrCodeValidator,
         analyticsKeySubmissionCollector = analyticsKeySubmissionCollector
@@ -74,22 +72,22 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() {
         val invalidQrCode = "https://no-guid-here"
 
         every { qrCodeValidator.validate(validQrCode) } returns coronaTestQRCode
-        every { qrCodeValidator.validate(invalidQrCode) } throws InvalidQRCodeException()
+
+        val expectedError = InvalidQRCodeException()
+        every { qrCodeValidator.validate(invalidQrCode) } throws expectedError
 
         val viewModel = createViewModel()
 
+        viewModel.qrCodeErrorEvent.observeForever {}
         // start
-        viewModel.qrCodeValidationState.value = ValidationState.STARTED
-
-        viewModel.qrCodeValidationState.value shouldBe ValidationState.STARTED
+        viewModel.qrCodeErrorEvent.value shouldBe null
 
         viewModel.registerCoronaTest(validQrCode)
-        viewModel.qrCodeValidationState.observeForever {}
-        viewModel.qrCodeValidationState.value shouldBe ValidationState.SUCCESS
+        viewModel.qrCodeErrorEvent.value shouldBe null
 
         // invalid guid
         viewModel.registerCoronaTest(invalidQrCode)
-        viewModel.qrCodeValidationState.value shouldBe ValidationState.INVALID
+        viewModel.qrCodeErrorEvent.value shouldBe expectedError
     }
 
     @Test
@@ -107,7 +105,6 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() {
 
             every { qrCodeValidator.validate(any()) } returns coronaTestQRCode
             every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } just Runs
-            coEvery { qrCodeRegistrationStateProcessor.startQrCodeRegistration(any(), any()) } just Runs
 
             createViewModel().registerCoronaTest(rawResult = "")
 
@@ -124,7 +121,6 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() {
 
             every { qrCodeValidator.validate(any()) } returns coronaTestQRCode
             every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } just Runs
-            coEvery { qrCodeRegistrationStateProcessor.startQrCodeRegistration(any(), any()) } just Runs
 
             createViewModel().registerCoronaTest(rawResult = "")
 
@@ -145,7 +141,6 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() {
 
             every { qrCodeValidator.validate(any()) } returns coronaTestQRCode
             every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } just Runs
-            coEvery { qrCodeRegistrationStateProcessor.startQrCodeRegistration(any(), any()) } just Runs
 
             createViewModel().registerCoronaTest(rawResult = "")
 
@@ -168,7 +163,6 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() {
 
             every { qrCodeValidator.validate(any()) } returns coronaTestQRCode
             every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } just Runs
-            coEvery { qrCodeRegistrationStateProcessor.startQrCodeRegistration(any(), any()) } just Runs
 
             createViewModel().registerCoronaTest(rawResult = "")
             verify(exactly = 0) {