From 65d893538904bb396a6a7aaeff88c3f7852e7874 Mon Sep 17 00:00:00 2001
From: Matthias Urhahn <matthias.urhahn@sap.com>
Date: Fri, 7 May 2021 17:36:32 +0200
Subject: [PATCH] Fix duplicate test removal error case handling
 (EXPOSUREAPP-7124) (#3115)

* Remove TransactionException.kt it is never being thrown, leftover from legacy code.

* On error, move to submission dispatcher screen, there the user could also be coming from a TAN.

* Refactor handling for errors that happen when the user tries to register the same test type again.

* Don't save a test that is already *_REDEMEED on the initial scan.

* Unit tests for RA/PCRCoronaTest.isFinal

Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com>
---
 .../exception/TransactionException.kt         |  21 --
 .../SubmissionDeletionWarningFragment.kt      | 155 ++++---------
 .../SubmissionDeletionWarningViewModel.kt     | 214 +++++++-----------
 .../QrCodeRegistrationStateProcessor.kt       |  11 +-
 .../submission/tan/SubmissionTanViewModel.kt  |   8 -
 .../src/main/res/navigation/nav_graph.xml     |   4 +-
 .../coronatest/type/pcr/PCRCoronaTestTest.kt  |  25 ++
 .../type/rapidantigen/RACoronaTestTest.kt     |  26 +++
 8 files changed, 190 insertions(+), 274 deletions(-)
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/TransactionException.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestTest.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTestTest.kt

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/TransactionException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/TransactionException.kt
deleted file mode 100644
index c7dd5daca..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/TransactionException.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package de.rki.coronawarnapp.exception
-
-import de.rki.coronawarnapp.exception.reporting.ErrorCodes
-import de.rki.coronawarnapp.exception.reporting.ReportedException
-import java.util.UUID
-
-/**
- * An Exception thrown when an error occurs inside the Transaction
- *
- * @param transactionId the atomic Transaction ID
- * @param state the atomic Transaction state (defined in the Transaction) with a valid ToString
- * @param cause the cause of the error
- *
- * @see de.rki.coronawarnapp.transaction.Transaction
- */
-class TransactionException constructor(transactionId: UUID, state: String, cause: Throwable?) :
-    ReportedException(
-        ErrorCodes.TRANSACTION_PROBLEM.code,
-        "An error occurred during execution of transaction $transactionId, State $state",
-        cause
-    )
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 03e2b7cd6..562c87cfc 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
@@ -7,14 +7,14 @@ import androidx.core.view.isVisible
 import androidx.fragment.app.Fragment
 import androidx.navigation.fragment.navArgs
 import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
+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.ui.submission.ApiRequestState
 import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
 import de.rki.coronawarnapp.util.DialogHelper
 import de.rki.coronawarnapp.util.di.AutoInject
@@ -24,6 +24,7 @@ import de.rki.coronawarnapp.util.ui.observe2
 import de.rki.coronawarnapp.util.ui.viewBindingLazy
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted
+import timber.log.Timber
 import javax.inject.Inject
 
 class SubmissionDeletionWarningFragment : Fragment(R.layout.fragment_submission_deletion_warning), AutoInject {
@@ -46,7 +47,6 @@ class SubmissionDeletionWarningFragment : Fragment(R.layout.fragment_submission_
         super.onViewCreated(view, savedInstanceState)
 
         binding.apply {
-
             when (viewModel.getTestType()) {
                 CoronaTest.Type.PCR -> {
                     headline.text = getString(R.string.submission_deletion_warning_headline_pcr_test)
@@ -59,122 +59,63 @@ class SubmissionDeletionWarningFragment : Fragment(R.layout.fragment_submission_
                 }
             }
 
-            continueButton.setOnClickListener {
-                viewModel.deleteExistingAndRegisterNewTest()
-            }
+            continueButton.setOnClickListener { viewModel.deleteExistingAndRegisterNewTest() }
 
-            toolbar.setNavigationOnClickListener {
-                viewModel.onCancelButtonClick()
-            }
-        }
-
-        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)
-
-            navigateToDispatchScreen()
+            toolbar.setNavigationOnClickListener { viewModel.onCancelButtonClick() }
         }
 
         viewModel.registrationState.observe2(this) { state ->
-            binding.submissionQrCodeScanSpinner.isVisible = state.apiRequestState == ApiRequestState.STARTED
-            binding.continueButton.isVisible = state.apiRequestState != ApiRequestState.STARTED
-
-            if (ApiRequestState.SUCCESS == state.apiRequestState) {
-
-                when (viewModel.getRegistrationType()) {
-                    SubmissionDeletionWarningViewModel.RegistrationType.QR -> {
-                        if (state.testResult == CoronaTestResult.PCR_POSITIVE) {
-                            viewModel.triggerNavigationToSubmissionTestResultAvailableFragment()
-                        } else {
-                            viewModel.triggerNavigationToSubmissionTestResultPendingFragment()
-                        }
-                    }
-                    SubmissionDeletionWarningViewModel.RegistrationType.TAN -> {
-                        doNavigate(
-                            SubmissionDeletionWarningFragmentDirections
-                                .actionSubmissionDeletionFragmentToSubmissionTestResultNoConsentFragment(
-                                    viewModel.getTestType()
-                                )
-                        )
-                    }
-                }
-            }
+            binding.submissionQrCodeScanSpinner.isVisible = state.isFetching
+            binding.continueButton.isVisible = !state.isFetching && state.coronaTest == null
         }
         viewModel.registrationError.observe2(this) {
-            DialogHelper.showDialog(buildErrorDialog(it))
+            showErrorDialog(it)
+            doNavigate(
+                SubmissionDeletionWarningFragmentDirections
+                    .actionSubmissionDeletionWarningFragmentToSubmissionDispatcherFragment()
+            )
         }
 
         viewModel.routeToScreen.observe2(this) {
-            when (it) {
-                SubmissionNavigationEvents.NavigateToConsent -> {
-                    doNavigate(
-                        SubmissionDeletionWarningFragmentDirections
-                            .actionSubmissionDeletionWarningFragmentToSubmissionConsentFragment()
-                    )
-                }
-                is SubmissionNavigationEvents.NavigateToResultAvailableScreen -> {
-                    doNavigate(
-                        SubmissionDeletionWarningFragmentDirections
-                            .actionSubmissionDeletionWarningFragmentToSubmissionTestResultAvailableFragment(
-                                testType = it.coronaTestType
-                            )
-                    )
-                }
-                is SubmissionNavigationEvents.NavigateToResultPendingScreen -> {
-                    doNavigate(
-                        SubmissionDeletionWarningFragmentDirections
-                            .actionSubmissionDeletionWarningFragmentToSubmissionTestResultPendingFragment(
-                                testType = it.coronaTestType
-                            )
-                    )
-                }
-            }
+            Timber.d("Navigating to %s", it)
+            doNavigate(it)
         }
     }
 
-    private fun navigateToDispatchScreen() =
-        doNavigate(
-            SubmissionDeletionWarningFragmentDirections
-                .actionSubmissionDeletionWarningFragmentToSubmissionDispatcherFragment()
-        )
-
-    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,
-                { /* startDecode() */ },
-                ::navigateToDispatchScreen
-            )
-            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 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()
     }
 
     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 9a46d82ae..4315b5004 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,24 +1,17 @@
 package de.rki.coronawarnapp.ui.submission.deletionwarning
 
-import androidx.annotation.VisibleForTesting
 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.server.CoronaTestResult
 import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
-import de.rki.coronawarnapp.exception.ExceptionCategory
-import de.rki.coronawarnapp.exception.TransactionException
-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.ui.submission.viewmodel.SubmissionNavigationEvents
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
@@ -26,154 +19,123 @@ import kotlinx.coroutines.flow.first
 import timber.log.Timber
 
 class SubmissionDeletionWarningViewModel @AssistedInject constructor(
-    private val coronaTestRepository: CoronaTestRepository,
-    private val submissionRepository: SubmissionRepository,
     @Assisted private val coronaTestQrCode: CoronaTestQRCode?,
     @Assisted private val coronaTestQrTan: CoronaTestTAN?,
-
     @Assisted private val isConsentGiven: Boolean,
+    private val submissionRepository: SubmissionRepository,
+    private val coronaTestRepository: CoronaTestRepository,
 ) : CWAViewModel() {
 
-    val routeToScreen = SingleLiveEvent<SubmissionNavigationEvents>()
-    val showRedeemedTokenWarning = SingleLiveEvent<Unit>()
-    private val mutableRegistrationState = MutableLiveData(RegistrationState(ApiRequestState.IDLE))
+    val routeToScreen = SingleLiveEvent<NavDirections>()
+    private val mutableRegistrationState = MutableLiveData(RegistrationState())
     val registrationState: LiveData<RegistrationState> = mutableRegistrationState
-    val registrationError = SingleLiveEvent<CwaWebException>()
+    val registrationError = SingleLiveEvent<Throwable>()
 
-    fun deleteExistingAndRegisterNewTest() = launch {
-        when (getRegistrationType()) {
-            RegistrationType.QR -> deleteExistingAndRegisterNewTestWithQrCode()
-            RegistrationType.TAN -> deleteExistingAndRegisterNewTestWitTAN()
-        }
+    private fun getRegistrationType(): RegistrationType = if (coronaTestQrCode != null) {
+        RegistrationType.QR
+    } else {
+        RegistrationType.TAN
     }
 
-    private suspend fun deleteExistingAndRegisterNewTestWithQrCode() {
-        try {
-            val currentTest = submissionRepository.testForType(coronaTestQrCode!!.type).first()
-            coronaTestRepository.removeTest(currentTest!!.identifier)
-            doDeviceRegistration(coronaTestQrCode)
-        } catch (err: Exception) {
-            Timber.e(err, "Removal of existing test failed with msg: ${err.message}")
-            err.report(ExceptionCategory.INTERNAL)
+    // 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
+
+    fun deleteExistingAndRegisterNewTest() = launch {
+        when {
+            coronaTestQrTan != null -> deleteExistingAndRegisterNewTestWitTAN()
+            else -> deleteExistingAndRegisterNewTestWithQrCode()
         }
     }
 
-    private suspend fun deleteExistingAndRegisterNewTestWitTAN() {
+    private suspend fun deleteExistingAndRegisterNewTestWithQrCode() = try {
+        requireNotNull(coronaTestQrCode) { "QR Code was unavailable" }
+
+        // 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?")
 
-        try {
-            val currentTest = submissionRepository.testForType(CoronaTest.Type.PCR).first()
-            coronaTestRepository.removeTest(currentTest!!.identifier)
-            onTanSubmit()
-        } catch (err: Exception) {
-            Timber.e(err, "Removal of existing test failed with msg: ${err.message}")
-            err.report(ExceptionCategory.INTERNAL)
+        mutableRegistrationState.postValue(RegistrationState(isFetching = true))
+
+        val coronaTest = submissionRepository.registerTest(coronaTestQrCode)
+
+        if (coronaTest.isFinal) {
+            Timber.d("New test was already final, 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()
         }
-    }
 
-    private suspend fun onTanSubmit() {
-
-        try {
-            mutableRegistrationState.postValue(RegistrationState(ApiRequestState.STARTED))
-            submissionRepository.registerTest(coronaTestQrTan!!)
-            mutableRegistrationState.postValue(RegistrationState(ApiRequestState.SUCCESS))
-        } catch (err: CwaWebException) {
-            mutableRegistrationState.postValue(RegistrationState(ApiRequestState.FAILED))
-            registrationError.postValue(err)
-        } catch (err: TransactionException) {
-            if (err.cause is CwaWebException) {
-                registrationError.postValue(err.cause)
-            } else {
-                err.report(ExceptionCategory.INTERNAL)
-            }
-            mutableRegistrationState.postValue(RegistrationState(ApiRequestState.FAILED))
-        } catch (err: Exception) {
-            mutableRegistrationState.postValue(RegistrationState(ApiRequestState.FAILED))
-            err.report(ExceptionCategory.INTERNAL)
+        if (isConsentGiven) {
+            submissionRepository.giveConsentToSubmission(type = coronaTestQrCode.type)
         }
-    }
 
-    data class RegistrationState(
-        val apiRequestState: ApiRequestState,
-        val testResult: CoronaTestResult? = null
-    )
+        continueWithNewTest(coronaTest)
 
-    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-    internal suspend fun doDeviceRegistration(coronaTestQRCode: CoronaTestQRCode) {
-        try {
-            mutableRegistrationState.postValue(RegistrationState(ApiRequestState.STARTED))
-            val coronaTest = submissionRepository.registerTest(coronaTestQRCode)
-            if (isConsentGiven) {
-                submissionRepository.giveConsentToSubmission(type = coronaTestQRCode.type)
-            }
-            checkTestResult(coronaTest.testResult)
-            mutableRegistrationState.postValue(
-                RegistrationState(
-                    ApiRequestState.SUCCESS,
-                    coronaTest.testResult
-                )
-            )
-        } catch (err: CwaWebException) {
-            Timber.e(err, "Msg: ${err.message}")
-            mutableRegistrationState.postValue(RegistrationState(ApiRequestState.FAILED))
-            registrationError.postValue(err)
-        } catch (err: TransactionException) {
-            Timber.e(err, "Msg: ${err.message}")
-            if (err.cause is CwaWebException) {
-                registrationError.postValue(err.cause)
-            } else {
-                err.report(ExceptionCategory.INTERNAL)
-            }
-            mutableRegistrationState.postValue(RegistrationState(ApiRequestState.FAILED))
-        } catch (err: InvalidQRCodeException) {
-            Timber.e(err, "Msg: ${err.message}")
-            mutableRegistrationState.postValue(RegistrationState(ApiRequestState.FAILED))
-            deregisterTestFromDevice(coronaTestQRCode)
-            showRedeemedTokenWarning.postValue(Unit)
-        } catch (err: Exception) {
-            Timber.e(err, "Msg: ${err.message}")
-            mutableRegistrationState.postValue(RegistrationState(ApiRequestState.FAILED))
-            err.report(ExceptionCategory.INTERNAL)
-        }
+        mutableRegistrationState.postValue(RegistrationState(coronaTest = coronaTest))
+    } catch (e: Exception) {
+        Timber.e(e, "Error during test registration via QR code")
+        mutableRegistrationState.postValue(RegistrationState(isFetching = false))
+        registrationError.postValue(e)
     }
 
-    fun onCancelButtonClick() {
-        routeToScreen.postValue(SubmissionNavigationEvents.NavigateToConsent)
-    }
+    private suspend fun deleteExistingAndRegisterNewTestWitTAN() = try {
+        requireNotNull(coronaTestQrTan) { "TAN was unavailable" }
 
-    fun triggerNavigationToSubmissionTestResultAvailableFragment() {
-        routeToScreen.postValue(SubmissionNavigationEvents.NavigateToResultAvailableScreen(coronaTestQrCode!!.type))
-    }
+        submissionRepository.testForType(CoronaTest.Type.PCR).first()?.let {
+            coronaTestRepository.removeTest(it.identifier)
+        } ?: Timber.w("Test we will replace with TAN was already removed?")
 
-    fun triggerNavigationToSubmissionTestResultPendingFragment() {
-        routeToScreen.postValue(SubmissionNavigationEvents.NavigateToResultPendingScreen(coronaTestQrCode!!.type))
-    }
+        mutableRegistrationState.postValue(RegistrationState(isFetching = true))
 
-    private fun checkTestResult(testResult: CoronaTestResult) {
-        if (testResult == CoronaTestResult.PCR_REDEEMED) {
-            throw InvalidQRCodeException()
-        }
-    }
+        val coronaTest = submissionRepository.registerTest(coronaTestQrTan)
+        continueWithNewTest(coronaTest)
 
-    private fun deregisterTestFromDevice(coronaTest: CoronaTestQRCode) {
-        launch {
-            Timber.d("deregisterTestFromDevice()")
+        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)
+    }
 
-            submissionRepository.removeTestFromDevice(type = coronaTest.type)
-            routeToScreen.postValue(SubmissionNavigationEvents.NavigateToMainActivity)
-        }
+    fun onCancelButtonClick() {
+        SubmissionDeletionWarningFragmentDirections
+            .actionSubmissionDeletionWarningFragmentToSubmissionConsentFragment()
+            .run { routeToScreen.postValue(this) }
     }
 
-    fun getRegistrationType(): RegistrationType {
-        return if (coronaTestQrCode != null) {
-            RegistrationType.QR
-        } else {
-            RegistrationType.TAN
+    private fun continueWithNewTest(coronaTest: CoronaTest) {
+        Timber.d("Continuing with our new CoronaTest: %s", coronaTest)
+        when (getRegistrationType()) {
+            RegistrationType.QR -> {
+                if (coronaTest.isPositive) {
+                    SubmissionDeletionWarningFragmentDirections
+                        .actionSubmissionDeletionWarningFragmentToSubmissionTestResultAvailableFragment(
+                            testType = coronaTestQrCode!!.type
+                        )
+                        .run { routeToScreen.postValue(this) }
+                } else {
+                    SubmissionDeletionWarningFragmentDirections
+                        .actionSubmissionDeletionWarningFragmentToSubmissionTestResultPendingFragment(
+                            testType = coronaTestQrCode!!.type
+                        )
+                        .run { routeToScreen.postValue(this) }
+                }
+            }
+            RegistrationType.TAN -> {
+                SubmissionDeletionWarningFragmentDirections
+                    .actionSubmissionDeletionFragmentToSubmissionTestResultNoConsentFragment(getTestType())
+                    .run { routeToScreen.postValue(this) }
+            }
         }
     }
 
-    fun getTestType(): CoronaTest.Type {
-        return coronaTestQrCode?.type ?: return CoronaTest.Type.PCR
-    }
+    data class RegistrationState(
+        val isFetching: Boolean = false,
+        val coronaTest: CoronaTest? = null,
+    )
 
     sealed class RegistrationType {
         object TAN : RegistrationType()
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
index 0328165d7..e0e8fd033 100644
--- 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
@@ -3,10 +3,8 @@ 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.server.CoronaTestResult
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.exception.ExceptionCategory
-import de.rki.coronawarnapp.exception.TransactionException
 import de.rki.coronawarnapp.exception.http.CwaWebException
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.submission.SubmissionRepository
@@ -45,13 +43,6 @@ class QrCodeRegistrationStateProcessor @Inject constructor(
         } catch (err: CwaWebException) {
             registrationState.postValue(RegistrationState(ApiRequestState.FAILED))
             registrationError.postValue(err)
-        } catch (err: TransactionException) {
-            if (err.cause is CwaWebException) {
-                registrationError.postValue(err.cause)
-            } else {
-                err.report(ExceptionCategory.INTERNAL)
-            }
-            registrationState.postValue(RegistrationState(ApiRequestState.FAILED))
         } catch (err: InvalidQRCodeException) {
             registrationState.postValue(RegistrationState(ApiRequestState.FAILED))
             Timber.d("deregisterTestFromDevice()")
@@ -63,7 +54,7 @@ class QrCodeRegistrationStateProcessor @Inject constructor(
         }
 
     private fun checkTestResult(request: CoronaTestQRCode, test: CoronaTest) {
-        if (test.testResult == CoronaTestResult.PCR_REDEEMED) {
+        if (test.isFinal) {
             throw InvalidQRCodeException("CoronaTestResult already redeemed ${request.registrationIdentifier}")
         }
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt
index de7b107fc..f92627014 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt
@@ -7,7 +7,6 @@ import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.exception.ExceptionCategory
-import de.rki.coronawarnapp.exception.TransactionException
 import de.rki.coronawarnapp.exception.http.CwaWebException
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.submission.SubmissionRepository
@@ -79,13 +78,6 @@ class SubmissionTanViewModel @AssistedInject constructor(
         } catch (err: CwaWebException) {
             registrationState.postValue(ApiRequestState.FAILED)
             registrationError.postValue(err)
-        } catch (err: TransactionException) {
-            if (err.cause is CwaWebException) {
-                registrationError.postValue(err.cause)
-            } else {
-                err.report(ExceptionCategory.INTERNAL)
-            }
-            registrationState.postValue(ApiRequestState.FAILED)
         } catch (err: Exception) {
             registrationState.postValue(ApiRequestState.FAILED)
             err.report(ExceptionCategory.INTERNAL)
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 d48804ccb..b5478d44e 100644
--- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
+++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
@@ -728,8 +728,8 @@
             app:nullable="true" />
         <action
             android:id="@+id/action_submissionDeletionWarningFragment_to_submissionDispatcherFragment"
-            app:popUpTo="@id/submissionQRCodeScanFragment"
-            app:popUpToInclusive="true" />
+            app:popUpTo="@id/submissionDispatcherFragment"
+            app:popUpToInclusive="false" />
         <action
             android:id="@+id/action_submissionDeletionFragment_to_submissionTestResultNoConsentFragment"
             app:destination="@id/submissionTestResultNoConsentFragment"
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestTest.kt
new file mode 100644
index 000000000..28353d256
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestTest.kt
@@ -0,0 +1,25 @@
+package de.rki.coronawarnapp.coronatest.type.pcr
+
+import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
+import io.kotest.matchers.shouldBe
+import org.joda.time.Instant
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class PCRCoronaTestTest : BaseTest() {
+
+    @Test
+    fun `a test is final if it reaches the REDEEMED state`() {
+        val instance = PCRCoronaTest(
+            identifier = "identifier",
+            lastUpdatedAt = Instant.EPOCH,
+            registeredAt = Instant.EPOCH,
+            registrationToken = "token",
+            testResult = CoronaTestResult.PCR_REDEEMED,
+        )
+
+        instance.isFinal shouldBe true
+        instance.copy(testResult = CoronaTestResult.PCR_POSITIVE).isFinal shouldBe false
+        instance.copy(testResult = CoronaTestResult.RAT_POSITIVE).isFinal shouldBe false
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTestTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTestTest.kt
new file mode 100644
index 000000000..d5f58239c
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTestTest.kt
@@ -0,0 +1,26 @@
+package de.rki.coronawarnapp.coronatest.type.rapidantigen
+
+import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
+import io.kotest.matchers.shouldBe
+import org.joda.time.Instant
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class RACoronaTestTest : BaseTest() {
+
+    @Test
+    fun `a test is final if it reaches the REDEEMED state`() {
+        val instance = RACoronaTest(
+            identifier = "identifier",
+            lastUpdatedAt = Instant.EPOCH,
+            registeredAt = Instant.EPOCH,
+            registrationToken = "token",
+            testResult = CoronaTestResult.RAT_REDEEMED,
+            testedAt = Instant.EPOCH,
+        )
+
+        instance.isFinal shouldBe true
+        instance.copy(testResult = CoronaTestResult.RAT_POSITIVE).isFinal shouldBe false
+        instance.copy(testResult = CoronaTestResult.PCR_REDEEMED).isFinal shouldBe false
+    }
+}
-- 
GitLab