From 13ac744e45df9cfd36e137874a8c34855a7d6e83 Mon Sep 17 00:00:00 2001
From: BMItter <46747780+BMItter@users.noreply.github.com>
Date: Fri, 9 Apr 2021 16:15:14 +0200
Subject: [PATCH] Automatic contact journal entry creation (EXPOSUREAPP-5943)
 (#2770)

* Created contact journal entry creator

* reduced complexity & clean

* updated CheckOutHandlerTest

* First test, others will follow

* ktlint

* added further tests for location creation

* Changed trace location id to ByteString

* 42

* ContactDiary ContactJournal contactdiaryoverviewViewModelTest Adjustment

* Improved location name, Added test if name gets created as expected

* test adjustment

* Create location visits if missing

* satisfy ci - further tests are coming

* clean

Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com>
---
 .../ContactDiaryDatabaseMigrationTest.kt      |   3 +-
 .../storage/ContactDiaryDatabaseTest.kt       |   3 +-
 .../model/ContactDiaryLocation.kt             |   3 +-
 .../model/DefaultContactDiaryLocation.kt      |   4 +-
 .../entity/ContactDiaryLocationEntity.kt      |   3 +-
 .../checkins/checkout/CheckOutHandler.kt      |  18 ++-
 .../ContactJournalCheckInEntryCreator.kt      | 104 ++++++++++++++++++
 .../util/database/CommonConverters.kt         |   8 ++
 .../ContactDiaryEditLocationsViewModelTest.kt |   3 +-
 .../ContactDiaryOverviewViewModelTest.kt      |   5 +-
 .../checkins/checkout/CheckOutHandlerTest.kt  |  53 ++++++++-
 .../ContactJournalCheckInEntryCreatorTest.kt  |  87 +++++++++++++++
 12 files changed, 272 insertions(+), 22 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreator.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreatorTest.kt

diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseMigrationTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseMigrationTest.kt
index dd683c4bb..ca8693d3e 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseMigrationTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseMigrationTest.kt
@@ -20,6 +20,7 @@ import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.runBlocking
+import okio.ByteString.Companion.decodeBase64
 import org.joda.time.Duration
 import org.joda.time.LocalDate
 import org.junit.Rule
@@ -176,7 +177,7 @@ class ContactDiaryDatabaseMigrationTest : BaseTestInstrumentation() {
             checkInID = null
         )
 
-        val locationAfter = location.copy(traceLocationID = "jshrgu-aifhioaio-aofsjof-samofp-kjsadngsgf")
+        val locationAfter = location.copy(traceLocationID = "jshrgu-aifhioaio-aofsjof-samofp-kjsadngsgf".decodeBase64())
         val locationVisitAfter = locationVisit.copy(checkInID = 101)
 
         val locationValues = ContentValues().apply {
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseTest.kt
index e3573457d..6cdc211e2 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseTest.kt
@@ -14,6 +14,7 @@ import io.kotest.matchers.shouldBe
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.runBlocking
+import okio.ByteString.Companion.decodeBase64
 import org.joda.time.Duration
 import org.joda.time.LocalDate
 import org.junit.After
@@ -37,7 +38,7 @@ class ContactDiaryDatabaseTest : BaseTestInstrumentation() {
         locationName = "Rewe Wiesloch",
         emailAddress = "location-emailAddress",
         phoneNumber = "location-phoneNumber",
-        traceLocationID = "a-b-c-d"
+        traceLocationID = "a-b-c-d".decodeBase64()
     )
     private val personEncounter = ContactDiaryPersonEncounterEntity(
         id = 3,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocation.kt
index e0486049d..020bb3f95 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocation.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocation.kt
@@ -1,5 +1,6 @@
 package de.rki.coronawarnapp.contactdiary.model
 
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationId
 import de.rki.coronawarnapp.util.lists.HasStableId
 import java.util.Locale
 
@@ -8,7 +9,7 @@ interface ContactDiaryLocation : HasStableId {
     var locationName: String
     val phoneNumber: String?
     val emailAddress: String?
-    val traceLocationID: String?
+    val traceLocationID: TraceLocationId?
 }
 
 fun List<ContactDiaryLocation>.sortByNameAndIdASC(): List<ContactDiaryLocation> =
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryLocation.kt
index c121e2611..9ebd6f415 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryLocation.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryLocation.kt
@@ -1,11 +1,13 @@
 package de.rki.coronawarnapp.contactdiary.model
 
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationId
+
 data class DefaultContactDiaryLocation(
     override val locationId: Long = 0L,
     override var locationName: String,
     override val phoneNumber: String? = null,
     override val emailAddress: String? = null,
-    override val traceLocationID: String? = null
+    override val traceLocationID: TraceLocationId? = null
 ) : ContactDiaryLocation {
     override val stableId: Long
         get() = locationId
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationEntity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationEntity.kt
index b09a84c8f..ba4f25464 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationEntity.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationEntity.kt
@@ -5,6 +5,7 @@ import androidx.room.ColumnInfo
 import androidx.room.Entity
 import androidx.room.PrimaryKey
 import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocation
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationId
 import de.rki.coronawarnapp.util.trimToLength
 import kotlinx.parcelize.Parcelize
 
@@ -15,7 +16,7 @@ data class ContactDiaryLocationEntity(
     @ColumnInfo(name = "locationName") override var locationName: String,
     override val phoneNumber: String?,
     override val emailAddress: String?,
-    @ColumnInfo(name = "traceLocationID") override val traceLocationID: String?
+    @ColumnInfo(name = "traceLocationID") override val traceLocationID: TraceLocationId?
 ) : ContactDiaryLocation, Parcelable {
     override val stableId: Long
         get() = locationId
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandler.kt
index 917e8c0ec..4852be4a7 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandler.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandler.kt
@@ -1,18 +1,18 @@
 package de.rki.coronawarnapp.presencetracing.checkins.checkout
 
-import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
+import dagger.Reusable
+import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
 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
+@Reusable
 class CheckOutHandler @Inject constructor(
     private val repository: CheckInRepository,
     private val timeStamper: TimeStamper,
-    private val diaryRepository: ContactDiaryRepository,
+    private val contactJournalCheckInEntryCreator: ContactJournalCheckInEntryCreator
 ) {
     /**
      * Throw **[IllegalArgumentException]** if the check-in does not exist.
@@ -21,18 +21,16 @@ class CheckOutHandler @Inject constructor(
     suspend fun checkOut(checkInId: Long, checkOutAt: Instant = timeStamper.nowUTC) {
         Timber.d("checkOut(checkInId=$checkInId, checkOutAt=%s)", checkOutAt)
 
-        var createJournalEntry = false
+        var checkIn: CheckIn? = null
         repository.updateCheckIn(checkInId) {
-            createJournalEntry = it.createJournalEntry
             it.copy(
                 checkInEnd = checkOutAt,
                 completed = true
-            )
+            ).also { c -> checkIn = c }
         }
 
-        if (createJournalEntry) {
-            Timber.d("Creating journal entry for $checkInId")
-            // TODO Create journal entry
+        if (checkIn?.createJournalEntry == true) {
+            contactJournalCheckInEntryCreator.createEntry(checkIn!!)
         }
 
         // Remove auto-checkout timer?
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreator.kt
new file mode 100644
index 000000000..e553c9834
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreator.kt
@@ -0,0 +1,104 @@
+package de.rki.coronawarnapp.presencetracing.checkins.checkout
+
+import androidx.annotation.VisibleForTesting
+import dagger.Reusable
+import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocation
+import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocationVisit
+import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocation
+import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocationVisit
+import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
+import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
+import de.rki.coronawarnapp.eventregistration.checkins.split.splitByMidnightUTC
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.firstOrNull
+import org.joda.time.Duration
+import org.joda.time.Seconds
+import org.joda.time.format.DateTimeFormat
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.math.roundToLong
+
+@Reusable
+class ContactJournalCheckInEntryCreator @Inject constructor(
+    private val diaryRepository: ContactDiaryRepository
+) {
+
+    suspend fun createEntry(checkIn: CheckIn) {
+        Timber.d("Creating journal entry for %s", checkIn)
+
+        // 1. Create location if missing
+        val location: ContactDiaryLocation = diaryRepository.locations.first()
+            .find { it.traceLocationID == checkIn.traceLocationId } ?: checkIn.toLocation()
+
+        // 2. Split CheckIn by Midnight UTC
+        val splitCheckIns = checkIn.splitByMidnightUTC()
+        Timber.d("Split %s into %s ", this, splitCheckIns)
+
+        // 3. Create LocationVisit if missing
+        splitCheckIns
+            .createMissingLocationVisits(location)
+            .forEach { diaryRepository.addLocationVisit(it) }
+    }
+
+    private suspend fun CheckIn.toLocation(): ContactDiaryLocation {
+        val location = DefaultContactDiaryLocation(
+            locationName = locationName(),
+            traceLocationID = traceLocationId
+        )
+        Timber.d("Created new location %s and adding it to contact journal db", location)
+        return diaryRepository.addLocation(location) // Get location from db cause we need the id autogenerated by db
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    fun CheckIn.locationName(): String {
+        val nameParts = mutableListOf(description, address)
+
+        if (traceLocationStart != null && traceLocationEnd != null) {
+            if (traceLocationStart.millis > 0 && traceLocationEnd.millis > 0) {
+                val formattedStartDate = traceLocationStart.toUserTimeZone().toString(DateTimeFormat.shortDateTime())
+                val formattedEndDate = traceLocationEnd.toUserTimeZone().toString(DateTimeFormat.shortDateTime())
+                nameParts.add("$formattedStartDate - $formattedEndDate")
+            }
+        }
+
+        return nameParts.joinToString(separator = ", ")
+    }
+
+    private fun CheckIn.toLocationVisit(location: ContactDiaryLocation): ContactDiaryLocationVisit {
+        // Use Seconds for more precision
+        val durationInMinutes = Seconds.secondsBetween(checkInStart, checkInEnd).seconds / 60.0
+        val duration = (durationInMinutes / 15).roundToLong() * 15
+        return DefaultContactDiaryLocationVisit(
+            date = checkInStart.toLocalDateUtc(),
+            contactDiaryLocation = location,
+            duration = Duration.standardMinutes(duration),
+            checkInID = id
+        )
+    }
+
+    private suspend fun List<CheckIn>.createMissingLocationVisits(location: ContactDiaryLocation):
+        List<ContactDiaryLocationVisit> {
+            Timber.d(
+                "createMissingLocationVisits(location=%s) for %s",
+                location,
+                this.joinToString(prefix = System.lineSeparator(), separator = System.lineSeparator())
+            )
+            val existingLocationVisits = diaryRepository.locationVisits.firstOrNull() ?: emptyList()
+            // Existing location visits shall not be updated, so just drop them
+            return filter {
+                existingLocationVisits.none { visit ->
+                    visit.date == it.checkInStart.toLocalDateUtc() &&
+                        visit.contactDiaryLocation.locationId == location.locationId
+                }
+            }
+                .map { it.toLocationVisit(location) }
+                .also {
+                    Timber.d(
+                        "Created locations visits: %s",
+                        it.joinToString(prefix = System.lineSeparator(), separator = System.lineSeparator())
+                    )
+                }
+        }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/database/CommonConverters.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/database/CommonConverters.kt
index 92a26a40a..4c30c5eb0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/database/CommonConverters.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/database/CommonConverters.kt
@@ -4,7 +4,9 @@ import androidx.room.TypeConverter
 import com.google.gson.Gson
 import com.google.gson.reflect.TypeToken
 import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationId
 import de.rki.coronawarnapp.util.serialization.fromJson
+import okio.ByteString.Companion.decodeBase64
 import org.joda.time.Instant
 import org.joda.time.LocalDate
 import org.joda.time.LocalTime
@@ -68,4 +70,10 @@ class CommonConverters {
 
     @TypeConverter
     fun fromLocationCode(code: LocationCode?): String? = code?.identifier
+
+    @TypeConverter
+    fun toTraceLocationId(value: String?): TraceLocationId? = value?.decodeBase64()
+
+    @TypeConverter
+    fun fromTraceLocationId(traceLocationId: TraceLocationId?): String? = traceLocationId?.base64()
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModelTest.kt
index e8ee5c327..6706173c4 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModelTest.kt
@@ -3,6 +3,7 @@ package de.rki.coronawarnapp.contactdiary.ui.edit
 import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocation
 import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryLocationEntity
 import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationId
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.Runs
@@ -30,7 +31,7 @@ class ContactDiaryEditLocationsViewModelTest {
         override val phoneNumber: String? = null
         override val emailAddress: String? = null
         override val stableId = 1L
-        override val traceLocationID: String? = null
+        override val traceLocationID: TraceLocationId? = null
     }
     private val locationList = listOf(location)
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt
index aa7c8b2d9..b50da8d26 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt
@@ -35,6 +35,7 @@ import io.mockk.mockk
 import io.mockk.runs
 import io.mockk.verify
 import kotlinx.coroutines.flow.flowOf
+import okio.ByteString.Companion.decodeBase64
 import org.joda.time.DateTimeZone
 import org.joda.time.Instant
 import org.joda.time.LocalDate
@@ -83,13 +84,13 @@ open class ContactDiaryOverviewViewModelTest {
     private val locationEventLowRisk = DefaultContactDiaryLocation(
         locationId = 456,
         locationName = "Jahrestreffen der deutschen SAP Anwendergruppe",
-        traceLocationID = "12ab-34cd-56ef-78gh-456"
+        traceLocationID = "12ab-34cd-56ef-78gh-456".decodeBase64()
     )
 
     private val locationEventHighRisk = DefaultContactDiaryLocation(
         locationId = 457,
         locationName = "Kiosk",
-        traceLocationID = "12ab-34cd-56ef-78gh-457"
+        traceLocationID = "12ab-34cd-56ef-78gh-457".decodeBase64()
     )
 
     private val locationEventLowRiskVisit = DefaultContactDiaryLocationVisit(
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt
index 04fbb3a38..07007ff99 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt
@@ -1,14 +1,16 @@
 package de.rki.coronawarnapp.presencetracing.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.coVerify
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import io.mockk.runs
 import kotlinx.coroutines.test.runBlockingTest
 import okio.ByteString.Companion.encode
 import org.joda.time.Instant
@@ -20,7 +22,7 @@ class CheckOutHandlerTest : BaseTest() {
 
     @MockK lateinit var repository: CheckInRepository
     @MockK lateinit var timeStamper: TimeStamper
-    @MockK lateinit var diaryRepository: ContactDiaryRepository
+    @MockK lateinit var contactJournalCheckInEntryCreator: ContactJournalCheckInEntryCreator
 
     private val testCheckIn = CheckIn(
         id = 42L,
@@ -39,6 +41,12 @@ class CheckOutHandlerTest : BaseTest() {
         completed = false,
         createJournalEntry = true
     )
+
+    private val testCheckInDontCreate = testCheckIn.copy(
+        id = 43L,
+        createJournalEntry = false
+    )
+
     private var updatedCheckIn: CheckIn? = null
     private val nowUTC = Instant.ofEpochMilli(50)
 
@@ -52,12 +60,19 @@ class CheckOutHandlerTest : BaseTest() {
             val callback: (CheckIn) -> CheckIn = arg(1)
             updatedCheckIn = callback(testCheckIn)
         }
+
+        coEvery { repository.updateCheckIn(43, any()) } coAnswers {
+            val callback: (CheckIn) -> CheckIn = arg(1)
+            updatedCheckIn = callback(testCheckInDontCreate)
+        }
+
+        coEvery { contactJournalCheckInEntryCreator.createEntry(any()) } just runs
     }
 
     private fun createInstance() = CheckOutHandler(
         repository = repository,
         timeStamper = timeStamper,
-        diaryRepository = diaryRepository,
+        contactJournalCheckInEntryCreator = contactJournalCheckInEntryCreator
     )
 
     @Test
@@ -68,7 +83,37 @@ class CheckOutHandlerTest : BaseTest() {
             checkInEnd = nowUTC,
             completed = true
         )
-        // TODO journal creation
+
+        coVerify(exactly = 1) {
+            contactJournalCheckInEntryCreator.createEntry(any())
+        }
+
         // TODO cancel auto checkouts
     }
+
+    @Test
+    fun `Creates entry if create journal entry is true`() = runBlockingTest {
+        createInstance().apply {
+            checkOut(42)
+        }
+
+        updatedCheckIn?.createJournalEntry shouldBe true
+
+        coVerify(exactly = 1) {
+            contactJournalCheckInEntryCreator.createEntry(any())
+        }
+    }
+
+    @Test
+    fun `Does not create entry if create journal entry is false`() = runBlockingTest {
+        createInstance().apply {
+            checkOut(43)
+        }
+
+        updatedCheckIn?.createJournalEntry shouldBe false
+
+        coVerify(exactly = 0) {
+            contactJournalCheckInEntryCreator.createEntry(any())
+        }
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreatorTest.kt
new file mode 100644
index 000000000..58834e6c7
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreatorTest.kt
@@ -0,0 +1,87 @@
+package de.rki.coronawarnapp.presencetracing.checkins.checkout
+
+import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocation
+import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
+import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
+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.just
+import io.mockk.runs
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runBlockingTest
+import okio.ByteString.Companion.encode
+import org.joda.time.Instant
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class ContactJournalCheckInEntryCreatorTest : BaseTest() {
+
+    @MockK lateinit var contactDiaryRepo: ContactDiaryRepository
+
+    private val testCheckIn = CheckIn(
+        id = 42L,
+        traceLocationId = "traceLocationId1".encode(),
+        version = 1,
+        type = 1,
+        description = "Restaurant",
+        address = "Around the corner",
+        traceLocationStart = Instant.parse("2021-03-04T22:00+01:00"),
+        traceLocationEnd = Instant.parse("2021-03-04T23:00+01:00"),
+        defaultCheckInLengthInMinutes = null,
+        cryptographicSeed = "cryptographicSeed".encode(),
+        cnPublicKey = "cnPublicKey",
+        checkInStart = Instant.parse("2021-03-04T22:00+01:00"),
+        checkInEnd = Instant.parse("2021-03-04T23:00+01:00"),
+        completed = false,
+        createJournalEntry = true
+    )
+
+    private val testLocation = DefaultContactDiaryLocation(
+        locationId = 123L,
+        locationName = "${testCheckIn.description}, ${testCheckIn.address}, ${testCheckIn.traceLocationStart} - ${testCheckIn.traceLocationEnd}",
+        traceLocationID = testCheckIn.traceLocationId
+    )
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        every { contactDiaryRepo.locations } returns flowOf(emptyList())
+
+        every { contactDiaryRepo.locationVisits } returns flowOf(emptyList())
+
+        coEvery { contactDiaryRepo.addLocationVisit(any()) } just runs
+
+        coEvery { contactDiaryRepo.addLocation(any()) } returns testLocation
+    }
+
+    private fun createInstance() = ContactJournalCheckInEntryCreator(
+        diaryRepository = contactDiaryRepo
+    )
+
+    @Test
+    fun `Creates location if missing`() = runBlockingTest {
+        every { contactDiaryRepo.locations } returns flowOf(emptyList()) andThen flowOf(listOf(testLocation))
+
+        // Repo returns an empty list for the first call, so location is missing and a new location should be created and added
+        val instance = createInstance()
+        instance.createEntry(testCheckIn)
+
+        coVerify(exactly = 1) {
+            contactDiaryRepo.addLocation(any())
+        }
+
+        // Location with trace location id already exists, so that location will be used
+        instance.createEntry(testCheckIn)
+        instance.createEntry(testCheckIn)
+        instance.createEntry(testCheckIn)
+
+        coVerify(exactly = 1) {
+            contactDiaryRepo.addLocation(any())
+        }
+    }
+}
-- 
GitLab