Skip to content
Snippets Groups Projects
Unverified Commit c619ba0b authored by Matthias Urhahn's avatar Matthias Urhahn Committed by GitHub
Browse files

Manual checkout, Check-In navigation (EXPOSUREAPP-5410) (#2680)


* Add checkout behavior, 1st draft.

* Add checkin edit navigation.

* Add tests.

* LINTs

* Basic error handling

* Fix stopship TODO

Co-authored-by: default avatarKolya Opahle <k.opahle@sap.com>
parent ae403447
Branches
Tags
No related merge requests found
Showing
with 191 additions and 17 deletions
package de.rki.coronawarnapp.eventregistration.checkins.checkout
import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
import de.rki.coronawarnapp.util.TimeStamper
import org.joda.time.Instant
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class CheckOutHandler @Inject constructor(
private val repository: CheckInRepository,
private val timeStamper: TimeStamper,
private val diaryRepository: ContactDiaryRepository,
) {
/**
* Throw **[IllegalArgumentException]** if the check-in does not exist.
* Could happen on raceconditions, you should catch this, should be rare though.
*/
suspend fun checkOut(checkInId: Long, checkOutAt: Instant = timeStamper.nowUTC) {
Timber.d("checkOut(checkInId=$checkInId)")
var createJournalEntry = false
repository.updateCheckIn(checkInId) {
createJournalEntry = it.createJournalEntry
it.copy(
checkInEnd = checkOutAt,
completed = true
)
}
if (createJournalEntry) {
Timber.d("Creating journal entry for $checkInId")
// TODO Create journal entry
}
// Remove auto-checkout timer?
}
}
......@@ -10,4 +10,8 @@ sealed class CheckInEvent {
object ConfirmRemoveAll : CheckInEvent()
data class ConfirmCheckIn(val verifiedTraceLocation: VerifiedTraceLocation) : CheckInEvent()
data class EditCheckIn(val checkInId: Long) : CheckInEvent()
object ShowInformation : CheckInEvent()
}
......@@ -22,6 +22,7 @@ import de.rki.coronawarnapp.util.CWADebug
import de.rki.coronawarnapp.util.di.AutoInject
import de.rki.coronawarnapp.util.lists.decorations.TopBottomPaddingDecorator
import de.rki.coronawarnapp.util.lists.diffutil.update
import de.rki.coronawarnapp.util.tryHumanReadableError
import de.rki.coronawarnapp.util.ui.doNavigate
import de.rki.coronawarnapp.util.ui.observe2
import de.rki.coronawarnapp.util.ui.viewBindingLazy
......@@ -95,7 +96,8 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
is CheckInEvent.ConfirmCheckIn -> {
doNavigate(
CheckInsFragmentDirections.actionCheckInsFragmentToConfirmCheckInFragment(
it.verifiedTraceLocation
verifiedTraceLocation = it.verifiedTraceLocation,
editCheckInId = 0,
)
)
}
......@@ -105,7 +107,23 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
is CheckInEvent.ConfirmRemoveAll -> {
showRemovalConfirmation(null)
}
is CheckInEvent.EditCheckIn -> {
doNavigate(
CheckInsFragmentDirections.actionCheckInsFragmentToConfirmCheckInFragment(
verifiedTraceLocation = null,
editCheckInId = it.checkInId,
)
)
}
is CheckInEvent.ShowInformation -> {
Toast.makeText(requireContext(), "TODO ¯\\_(ツ)_/¯", Toast.LENGTH_SHORT).show()
}
}
}
viewModel.errorEvent.observe2(this) {
val errorForHumans = it.tryHumanReadableError(requireContext())
Toast.makeText(requireContext(), errorForHumans.description, Toast.LENGTH_LONG).show()
}
}
......
......@@ -7,6 +7,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
import de.rki.coronawarnapp.eventregistration.checkins.checkout.CheckOutHandler
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeUriParser
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationQRCodeVerifier
import de.rki.coronawarnapp.exception.ExceptionCategory
......@@ -30,24 +31,28 @@ class CheckInsViewModel @AssistedInject constructor(
private val traceLocationQRCodeVerifier: TraceLocationQRCodeVerifier,
private val qrCodeUriParser: QRCodeUriParser,
private val checkInsRepository: CheckInRepository,
private val checkOutHandler: CheckOutHandler,
) : CWAViewModel(dispatcherProvider) {
val events = SingleLiveEvent<CheckInEvent>()
val errorEvent = SingleLiveEvent<Throwable>()
val checkins = checkInsRepository.allCheckIns
.map { checkins -> checkins.sortedBy { it.checkInEnd } }
.map { checkins ->
checkins.sortedWith(compareBy<CheckIn> { it.completed }.thenByDescending { it.checkInEnd })
}
.map { checkins ->
checkins.map { checkin ->
when {
!checkin.completed -> ActiveCheckInVH.Item(
checkin = checkin,
onCardClicked = { /* TODO */ },
onCardClicked = { events.postValue(CheckInEvent.EditCheckIn(it.id)) },
onRemoveItem = { events.postValue(CheckInEvent.ConfirmRemoveItem(it)) },
onCheckout = { /* TODO */ }
onCheckout = { doCheckOutNow(it) }
)
else -> PastCheckInVH.Item(
checkin = checkin,
onCardClicked = { /* TODO */ },
onCardClicked = { events.postValue(CheckInEvent.EditCheckIn(it.id)) },
onRemoveItem = { events.postValue(CheckInEvent.ConfirmRemoveItem(it)) }
)
}
......@@ -67,6 +72,16 @@ class CheckInsViewModel @AssistedInject constructor(
savedState.set(SKEY_LAST_DEEPLINK, deepLink)
}
private fun doCheckOutNow(checkIn: CheckIn) = launch(scope = appScope) {
Timber.d("doCheckOutNow(checkIn=%s)", checkIn)
try {
checkOutHandler.checkOut(checkIn.id)
} catch (e: Exception) {
Timber.e(e, "Checkout failed for %s", checkIn)
errorEvent.postValue(e)
}
}
fun onRemoveCheckInConfirmed(checkIn: CheckIn?) {
Timber.d("removeCheckin(checkIn=%s)", checkIn)
launch(scope = appScope) {
......
......@@ -67,6 +67,10 @@ class ActiveCheckInVH(parent: ViewGroup) :
else -> false
}
}
checkoutAction.setOnClickListener { item.onCheckout(item.checkin) }
itemView.setOnClickListener { item.onCardClicked(item.checkin) }
}
data class Item(
......
......@@ -5,7 +5,6 @@ import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.TraceLocationAttendeeCheckinsItemPastBinding
import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone
import org.joda.time.Instant
import org.joda.time.format.DateTimeFormat
class PastCheckInVH(parent: ViewGroup) :
......@@ -23,7 +22,7 @@ class PastCheckInVH(parent: ViewGroup) :
payloads: List<Any>
) -> Unit = { item, _ ->
val checkInStartUserTZ = item.checkin.checkInStart.toUserTimeZone()
val checkInEndUserTZ = (item.checkin.checkInEnd ?: Instant.EPOCH).toUserTimeZone()
val checkInEndUserTZ = item.checkin.checkInEnd.toUserTimeZone()
description.text = item.checkin.description
address.text = item.checkin.address
......@@ -42,6 +41,8 @@ class PastCheckInVH(parent: ViewGroup) :
else -> false
}
}
itemView.setOnClickListener { item.onCardClicked(item.checkin) }
}
data class Item(
......
......@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.ui.eventregistration.attendee.confirm
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.navArgs
import de.rki.coronawarnapp.R
......@@ -24,7 +25,8 @@ class ConfirmCheckInFragment : Fragment(R.layout.fragment_confirm_check_in), Aut
factoryProducer = { viewModelFactory },
constructorCall = { factory, savedState ->
factory as ConfirmCheckInViewModel.Factory
factory.create(navArgs.verifiedTraceLocation)
val editId = if (navArgs.editCheckInId == 0L) null else navArgs.editCheckInId
factory.create(navArgs.verifiedTraceLocation, editId)
}
)
private val binding: FragmentConfirmCheckInBinding by viewBindingLazy()
......@@ -37,13 +39,19 @@ class ConfirmCheckInFragment : Fragment(R.layout.fragment_confirm_check_in), Aut
toolbar.setNavigationOnClickListener { viewModel.onClose() }
confirmButton.setOnClickListener { viewModel.onConfirmTraceLocation() }
// TODO bind final UI
val traceLocation = args.verifiedTraceLocation.traceLocation
args.verifiedTraceLocation?.let {
val traceLocation = it.traceLocation
eventGuid.text = "GUID: %s".format(traceLocation.guid)
startTime.text = "Start time: %s".format(traceLocation.startDate)
endTime.text = "End time: %s".format(traceLocation.endDate)
description.text = "Description: %s".format(traceLocation.description)
}
if (navArgs.editCheckInId != 0L) {
Toast.makeText(requireContext(), "EDIT CHECKIN MODE", Toast.LENGTH_SHORT).show()
}
}
viewModel.events.observe2(this) { navEvent ->
when (navEvent) {
ConfirmCheckInNavigation.BackNavigation -> popBackStack()
......
......@@ -14,7 +14,8 @@ import org.joda.time.Duration
import org.joda.time.Instant
class ConfirmCheckInViewModel @AssistedInject constructor(
@Assisted private val verifiedTraceLocation: VerifiedTraceLocation,
@Assisted private val verifiedTraceLocation: VerifiedTraceLocation?,
@Assisted private val editCheckInId: Long?,
private val checkInRepository: CheckInRepository
) : CWAViewModel() {
......@@ -25,6 +26,7 @@ class ConfirmCheckInViewModel @AssistedInject constructor(
}
fun onConfirmTraceLocation() {
if (verifiedTraceLocation == null) return
launch {
// TODO This is only for testing
checkInRepository.addCheckIn(verifiedTraceLocation.toCheckIn(checkInStart = Instant.now()))
......@@ -60,7 +62,8 @@ class ConfirmCheckInViewModel @AssistedInject constructor(
@AssistedFactory
interface Factory : CWAViewModelFactory<ConfirmCheckInViewModel> {
fun create(
verifiedTraceLocation: VerifiedTraceLocation
verifiedTraceLocation: VerifiedTraceLocation?,
editCheckInId: Long?
): ConfirmCheckInViewModel
}
}
......@@ -11,7 +11,11 @@
tools:layout="@layout/fragment_confirm_check_in">
<argument
android:name="verifiedTraceLocation"
app:argType="de.rki.coronawarnapp.eventregistration.checkins.qrcode.VerifiedTraceLocation" />
app:argType="de.rki.coronawarnapp.eventregistration.checkins.qrcode.VerifiedTraceLocation"
app:nullable="true" />
<argument
android:name="editCheckInId"
app:argType="long" />
</fragment>
<fragment
android:id="@+id/scanCheckInQrCodeFragment"
......
......@@ -27,7 +27,8 @@ class ConfirmCheckInViewModelTest : BaseTest() {
MockKAnnotations.init(this)
viewModel = ConfirmCheckInViewModel(
verifiedTraceLocation = verifiedTraceLocation,
checkInRepository = checkInRepository
checkInRepository = checkInRepository,
editCheckInId = null
)
}
......
package de.rki.coronawarnapp.eventregistration.checkins.checkout
import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
import de.rki.coronawarnapp.util.TimeStamper
import io.kotest.matchers.shouldBe
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.every
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.test.runBlockingTest
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.joda.time.Instant
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
class CheckOutHandlerTest : BaseTest() {
@MockK lateinit var repository: CheckInRepository
@MockK lateinit var timeStamper: TimeStamper
@MockK lateinit var diaryRepository: ContactDiaryRepository
private val testCheckIn = CheckIn(
id = 42L,
guid = "eventOne",
guidHash = ByteString.EMPTY,
version = 1,
type = 1,
description = "Restaurant",
address = "Around the corner",
traceLocationStart = null,
traceLocationEnd = null,
defaultCheckInLengthInMinutes = null,
traceLocationBytes = ByteString.EMPTY,
signature = "signature".toByteArray().toByteString(),
checkInStart = Instant.EPOCH,
checkInEnd = Instant.EPOCH.plus(100),
completed = false,
createJournalEntry = true
)
private var updatedCheckIn: CheckIn? = null
private val nowUTC = Instant.ofEpochMilli(50)
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
every { timeStamper.nowUTC } returns nowUTC
coEvery { repository.updateCheckIn(42, any()) } coAnswers {
val callback: (CheckIn) -> CheckIn = arg(1)
updatedCheckIn = callback(testCheckIn)
}
}
private fun createInstance() = CheckOutHandler(
repository = repository,
timeStamper = timeStamper,
diaryRepository = diaryRepository,
)
@Test
fun `manual checkout`() = runBlockingTest {
val instance = createInstance()
instance.checkOut(42)
updatedCheckIn shouldBe testCheckIn.copy(
checkInEnd = nowUTC,
completed = true
)
// TODO journal creation
// TODO cancel auto checkouts
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment