diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/SerializationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/SerializationModule.kt
index e41065b3c8ee61e11a10d85ea24fdbf3c0ed4fff..1a35bb0361d6c87e14447419bebb714fb747f23b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/SerializationModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/SerializationModule.kt
@@ -6,9 +6,11 @@ import dagger.Module
 import dagger.Provides
 import dagger.Reusable
 import de.rki.coronawarnapp.util.serialization.adapter.ByteArrayAdapter
+import de.rki.coronawarnapp.util.serialization.adapter.ByteStringBase64Adapter
 import de.rki.coronawarnapp.util.serialization.adapter.DurationAdapter
 import de.rki.coronawarnapp.util.serialization.adapter.InstantAdapter
 import de.rki.coronawarnapp.util.serialization.adapter.LocalDateAdapter
+import okio.ByteString
 import org.joda.time.Duration
 import org.joda.time.Instant
 import org.joda.time.LocalDate
@@ -24,5 +26,6 @@ class SerializationModule {
         .registerTypeAdapter(LocalDate::class.java, LocalDateAdapter())
         .registerTypeAdapter(Duration::class.java, DurationAdapter())
         .registerTypeAdapter(ByteArray::class.java, ByteArrayAdapter())
+        .registerTypeAdapter(ByteString::class.java, ByteStringBase64Adapter())
         .create()
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/adapter/ByteStringBase64Adapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/adapter/ByteStringBase64Adapter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a4abf80511d4ea75b9e08aa093ae50a9d8ad0b6f
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/adapter/ByteStringBase64Adapter.kt
@@ -0,0 +1,24 @@
+package de.rki.coronawarnapp.util.serialization.adapter
+
+import com.google.gson.JsonParseException
+import com.google.gson.TypeAdapter
+import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonWriter
+import okio.ByteString
+import okio.ByteString.Companion.decodeBase64
+import org.json.JSONObject.NULL
+
+class ByteStringBase64Adapter : TypeAdapter<ByteString>() {
+    override fun write(out: JsonWriter, value: ByteString?) {
+        if (value == null) out.nullValue()
+        else value.base64().let { out.value(it) }
+    }
+
+    override fun read(reader: JsonReader): ByteString? = when (reader.peek()) {
+        NULL -> reader.nextNull().let { null }
+        else -> {
+            val raw = reader.nextString()
+            raw.decodeBase64() ?: throw JsonParseException("Can't decode base64 ByteString: $raw")
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/ProofCertificate.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/ProofCertificate.kt
index c11b906f2bcd77cfa50e5410d5f3f790039fc43a..006f67b7ecae3ac0a8e40f73addb29ce59deec54 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/ProofCertificate.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/ProofCertificate.kt
@@ -1,7 +1,27 @@
 package de.rki.coronawarnapp.vaccination.core
 
 import org.joda.time.Instant
+import org.joda.time.LocalDate
+
+interface ProofCertificate {
+    val personIdentifier: VaccinatedPersonIdentifier
 
-data class ProofCertificate(
     val expiresAt: Instant
-)
+
+    val firstName: String?
+    val lastName: String
+
+    val dateOfBirth: LocalDate
+
+    val vaccineName: String
+    val medicalProductName: String
+    val vaccineManufacturer: String
+
+    val doseNumber: Int
+    val totalSeriesOfDoses: Int
+
+    val vaccinatedAt: LocalDate
+
+    val certificateIssuer: String
+    val certificateId: String
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPerson.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPerson.kt
index 57190e9edfad71b42f3c993c753b5a0f403a09fe..fc29037ffa37a26efb60cd0a22a0868ef7a16af0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPerson.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPerson.kt
@@ -1,32 +1,45 @@
 package de.rki.coronawarnapp.vaccination.core
 
-import org.joda.time.Instant
+import de.rki.coronawarnapp.vaccination.core.repository.storage.PersonData
+import de.rki.coronawarnapp.vaccination.core.server.VaccinationValueSet
 import org.joda.time.LocalDate
 
 data class VaccinatedPerson(
-    val vaccinationCertificates: Set<VaccinationCertificate>,
-    val proofCertificates: Set<ProofCertificate>,
-    val isRefreshing: Boolean,
-    val lastUpdatedAt: Instant,
+    internal val data: PersonData,
+    private val valueSet: VaccinationValueSet?,
+    val isUpdatingData: Boolean = false,
+    val lastError: Throwable? = null,
 ) {
-    val identifier: VaccinatedPersonIdentifier = ""
+    val identifier: VaccinatedPersonIdentifier
+        get() = data.identifier
 
-    val firstName: String
-        get() = ""
+    val vaccinationStatus: Status
+        get() = if (proofCertificates.isNotEmpty()) Status.COMPLETE else Status.INCOMPLETE
+
+    val vaccinationCertificates: Set<VaccinationCertificate>
+        get() = data.vaccinations.map {
+            it.toVaccinationCertificate(valueSet)
+        }.toSet()
+
+    val proofCertificates: Set<ProofCertificate>
+        get() = data.proofs.map {
+            it.toProofCertificate(valueSet)
+        }.toSet()
+
+    val firstName: String?
+        get() = vaccinationCertificates.first().firstName
 
     val lastName: String
-        get() = ""
+        get() = vaccinationCertificates.first().lastName
 
     val dateOfBirth: LocalDate
-        get() = LocalDate.now()
+        get() = vaccinationCertificates.first().dateOfBirth
 
-    val vaccinationStatus: Status
-        get() = if (proofCertificates.isNotEmpty()) Status.COMPLETE else Status.INCOMPLETE
+    val isEligbleForProofCertificate: Boolean
+        get() = data.isEligbleForProofCertificate
 
     enum class Status {
         INCOMPLETE,
         COMPLETE
     }
 }
-
-typealias VaccinatedPersonIdentifier = String
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifier.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifier.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7629fcf8eec2503b8ce1bcebacbe0c4b06c5df6e
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifier.kt
@@ -0,0 +1,56 @@
+package de.rki.coronawarnapp.vaccination.core
+
+import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode
+import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateV1
+import de.rki.coronawarnapp.vaccination.core.repository.errors.VaccinationDateOfBirthMissmatchException
+import de.rki.coronawarnapp.vaccination.core.repository.errors.VaccinationNameMissmatchException
+import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateV1
+import org.joda.time.LocalDate
+
+data class VaccinatedPersonIdentifier(
+    val dateOfBirth: LocalDate,
+    val lastNameStandardized: String,
+    val firstNameStandardized: String?
+) {
+    val code: String by lazy {
+        val dob = dateOfBirth.toString()
+        val lastName = lastNameStandardized
+        val firstName = firstNameStandardized
+        "$dob#$lastName#$firstName"
+    }
+
+    fun requireMatch(other: VaccinatedPersonIdentifier) {
+        if (lastNameStandardized != other.lastNameStandardized) {
+            throw VaccinationNameMissmatchException(
+                "Family name does not match, got ${other.lastNameStandardized}, expected $lastNameStandardized"
+            )
+        }
+        if (firstNameStandardized != other.firstNameStandardized) {
+            throw VaccinationNameMissmatchException(
+                "Given name does not match, got ${other.firstNameStandardized}, expected $firstNameStandardized"
+            )
+        }
+        if (dateOfBirth != other.dateOfBirth) {
+            throw VaccinationDateOfBirthMissmatchException(
+                "Date of birth does not match, got ${other.dateOfBirth}, expected $dateOfBirth"
+            )
+        }
+    }
+}
+
+val VaccinationCertificateV1.personIdentifier: VaccinatedPersonIdentifier
+    get() = VaccinatedPersonIdentifier(
+        dateOfBirth = dateOfBirth,
+        lastNameStandardized = nameData.familyNameStandardized,
+        firstNameStandardized = nameData.givenNameStandardized
+    )
+
+val ProofCertificateV1.personIdentifier: VaccinatedPersonIdentifier
+    get() = VaccinatedPersonIdentifier(
+        dateOfBirth = dateOfBirth,
+        lastNameStandardized = nameData.familyNameStandardized,
+        firstNameStandardized = nameData.givenNameStandardized
+    )
+
+val VaccinationCertificateQRCode.personIdentifier: VaccinatedPersonIdentifier
+    get() = parsedData.vaccinationCertificate.personIdentifier
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationCertificate.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationCertificate.kt
index 2a8025ba3ca547ce56ff13905ee7dd449502dfe2..990316b2263122938888448170fee58831c5680a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationCertificate.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationCertificate.kt
@@ -1,20 +1,24 @@
 package de.rki.coronawarnapp.vaccination.core
 
 import de.rki.coronawarnapp.ui.Country
-import org.joda.time.Instant
 import org.joda.time.LocalDate
 
-data class VaccinationCertificate(
-    val firstName: String,
-    val lastName: String,
-    val dateOfBirth: LocalDate,
-    val vaccinatedAt: Instant,
-    val vaccineName: String,
-    val vaccineManufacturer: String,
-    val chargeId: String,
-    val certificateIssuer: String,
-    val certificateCountry: Country,
-    val certificateId: String,
-) {
-    val identifier: VaccinatedPersonIdentifier get() = ""
+interface VaccinationCertificate {
+    val firstName: String?
+    val lastName: String
+    val dateOfBirth: LocalDate
+    val vaccinatedAt: LocalDate
+
+    val vaccineName: String
+    val vaccineManufacturer: String
+    val medicalProductName: String
+
+    val doseNumber: Int
+    val totalSeriesOfDoses: Int
+
+    val certificateIssuer: String
+    val certificateCountry: Country
+    val certificateId: String
+
+    val personIdentifier: VaccinatedPersonIdentifier
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationException.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4963bf1bd500fddac6397d6da57a8f06a5d8eaf5
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationException.kt
@@ -0,0 +1,6 @@
+package de.rki.coronawarnapp.vaccination.core
+
+open class VaccinationException(
+    cause: Throwable?,
+    message: String
+) : Exception(message, cause)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a88454e14122585eea2c2c91cd35b78c8bc648ad
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt
@@ -0,0 +1,39 @@
+package de.rki.coronawarnapp.vaccination.core.qrcode
+
+import okio.ByteString
+import org.joda.time.LocalDate
+
+class VaccinationCertificateCOSEParser {
+
+    fun parse(vaccinationCOSE: ByteString): VaccinationCertificateData {
+        // TODO
+        val cert = VaccinationCertificateV1(
+            version = "1.0.0",
+            nameData = VaccinationCertificateV1.NameData(
+                givenName = "François-Joan",
+                givenNameStandardized = "FRANCOIS<JOAN",
+                familyName = "d'Arsøns - van Halen",
+                familyNameStandardized = "DARSONS<VAN<HALEN",
+            ),
+            dateOfBirth = LocalDate.parse("2009-02-28"),
+            vaccinationDatas = listOf(
+                VaccinationCertificateV1.VaccinationData(
+                    targetId = "840539006",
+                    vaccineId = "1119349007",
+                    medicalProductId = "EU/1/20/1528",
+                    marketAuthorizationHolderId = "ORG-100030215",
+                    doseNumber = 1,
+                    totalSeriesOfDoses = 2,
+                    vaccinatedAt = LocalDate.parse("2021-04-21"),
+                    countryOfVaccination = "NL",
+                    certificateIssuer = "Ministry of Public Health, Welfare and Sport",
+                    uniqueCertificateIdentifier = "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ",
+                )
+            ),
+        )
+
+        return VaccinationCertificateData(
+            vaccinationCertificate = cert
+        )
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateData.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b22ec8052f825ecc91a4b6edae44c9d9d80dca5d
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateData.kt
@@ -0,0 +1,9 @@
+package de.rki.coronawarnapp.vaccination.core.qrcode
+
+/**
+ * Represents the information gained from data in COSE representation
+ */
+data class VaccinationCertificateData constructor(
+    // Parsed json
+    val vaccinationCertificate: VaccinationCertificateV1
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateQRCode.kt
index ad4ff24d66c095b366fc303af897299321717b59..89a0837d36b49162cd8595339bd9816749be648b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateQRCode.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateQRCode.kt
@@ -1,9 +1,13 @@
 package de.rki.coronawarnapp.vaccination.core.qrcode
 
+import okio.ByteString
+
 // TODO
 data class VaccinationCertificateQRCode(
-    // Vaccine or prophylaxis
-    val vaccineNameId: String,
-    val vaccineMedicinalProduct: String,
-    val marketAuthorizationHolder: String,
-)
+    val parsedData: VaccinationCertificateData,
+    // COSE representation of the vaccination certificate (as byte sequence)
+    val certificateCOSE: ByteString,
+) {
+    val uniqueCertificateIdentifier: String
+        get() = parsedData.vaccinationCertificate.vaccinationDatas.single().uniqueCertificateIdentifier
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1.kt
new file mode 100644
index 0000000000000000000000000000000000000000..777fe822e5bbbe3f9a537b52afc597b8d4325082
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1.kt
@@ -0,0 +1,42 @@
+package de.rki.coronawarnapp.vaccination.core.qrcode
+
+import com.google.gson.annotations.SerializedName
+import org.joda.time.LocalDate
+
+data class VaccinationCertificateV1(
+    @SerializedName("ver") val version: String,
+    @SerializedName("nam") val nameData: NameData,
+    @SerializedName("dob") val dateOfBirth: LocalDate,
+    @SerializedName("v") val vaccinationDatas: List<VaccinationData>,
+) {
+
+    data class NameData(
+        @SerializedName("fn") val familyName: String?,
+        @SerializedName("fnt") val familyNameStandardized: String,
+        @SerializedName("gn") val givenName: String?,
+        @SerializedName("gnt") val givenNameStandardized: String?,
+    )
+
+    data class VaccinationData(
+        // Disease or agent targeted, e.g. "tg": "840539006"
+        @SerializedName("tg") val targetId: String,
+        // Vaccine or prophylaxis, e.g. "vp": "1119349007"
+        @SerializedName("vp") val vaccineId: String,
+        // Vaccine medicinal product,e.g. "mp": "EU/1/20/1528",
+        @SerializedName("mp") val medicalProductId: String,
+        // Marketing Authorization Holder, e.g. "ma": "ORG-100030215",
+        @SerializedName("ma") val marketAuthorizationHolderId: String,
+        // Dose Number, e.g. "dn": 2
+        @SerializedName("dn") val doseNumber: Int,
+        // Total Series of Doses, e.g. "sd": 2,
+        @SerializedName("sd") val totalSeriesOfDoses: Int,
+        // Date of Vaccination, e.g. "dt" : "2021-04-21"
+        @SerializedName("dt") val vaccinatedAt: LocalDate,
+        // Country of Vaccination, e.g. "co": "NL"
+        @SerializedName("co") val countryOfVaccination: String,
+        // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport",
+        @SerializedName("is") val certificateIssuer: String,
+        // Unique Certificate Identifier, e.g.  "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ"
+        @SerializedName("ci") val uniqueCertificateIdentifier: String
+    )
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt
index 1fdf66e001a7a7629d0d7ccf82976f4221ac996e..c4b6a7fb8536a0def5677044033c3cad525cee58 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt
@@ -1,85 +1,207 @@
 package de.rki.coronawarnapp.vaccination.core.repository
 
-import de.rki.coronawarnapp.ui.Country
+import de.rki.coronawarnapp.bugreporting.reportProblem
+import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.coroutine.AppScope
-import de.rki.coronawarnapp.vaccination.core.ProofCertificate
+import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import de.rki.coronawarnapp.util.flow.HotDataFlow
+import de.rki.coronawarnapp.util.flow.combine
 import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson
+import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier
 import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate
+import de.rki.coronawarnapp.vaccination.core.personIdentifier
 import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode
+import de.rki.coronawarnapp.vaccination.core.repository.errors.VaccinatedPersonNotFoundException
+import de.rki.coronawarnapp.vaccination.core.repository.errors.VaccinationCertificateNotFoundException
+import de.rki.coronawarnapp.vaccination.core.repository.storage.PersonData
+import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationStorage
+import de.rki.coronawarnapp.vaccination.core.repository.storage.toProofContainer
+import de.rki.coronawarnapp.vaccination.core.repository.storage.toVaccinationContainer
+import de.rki.coronawarnapp.vaccination.core.server.VaccinationProofServer
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.launch
-import org.joda.time.Instant
-import org.joda.time.LocalDate
+import kotlinx.coroutines.plus
+import kotlinx.coroutines.withContext
+import timber.log.Timber
 import javax.inject.Inject
 import javax.inject.Singleton
 
 @Singleton
 class VaccinationRepository @Inject constructor(
-    @AppScope private val scope: CoroutineScope,
+    @AppScope private val appScope: CoroutineScope,
+    dispatcherProvider: DispatcherProvider,
+    private val timeStamper: TimeStamper,
+    private val storage: VaccinationStorage,
+    private val valueSetsRepository: ValueSetsRepository,
+    private val vaccinationProofServer: VaccinationProofServer,
 ) {
 
-    private val vc = VaccinationCertificate(
-        firstName = "Max",
-        lastName = "Mustermann",
-        dateOfBirth = LocalDate.now(),
-        vaccinatedAt = Instant.now(),
-        vaccineName = "Comirnaty (mRNA)",
-        vaccineManufacturer = "BioNTech",
-        chargeId = "CB2342",
-        certificateIssuer = "Landratsamt Potsdam",
-        certificateCountry = Country.DE,
-        certificateId = "05930482748454836478695764787840"
-    )
-
-    private val vc1 = VaccinationCertificate(
-        firstName = "Max",
-        lastName = "Mustermann",
-        dateOfBirth = LocalDate.now(),
-        vaccinatedAt = Instant.now(),
-        vaccineName = "Comirnaty (mRNA)",
-        vaccineManufacturer = "BioNTech",
-        chargeId = "CB2342",
-        certificateIssuer = "Landratsamt Potsdam",
-        certificateCountry = Country.DE,
-        certificateId = "05930482748454836478695764787841"
-    )
-
-    private val pc = ProofCertificate(
-        expiresAt = Instant.now()
-    )
-
-    // TODO read from repos
-    val vaccinationInfos: Flow<Set<VaccinatedPerson>> = flowOf(
-        setOf(
-            VaccinatedPerson(
-                setOf(vc),
-                setOf(),
-                isRefreshing = false,
-                lastUpdatedAt = Instant.now()
-            ),
-            VaccinatedPerson(
-                setOf(vc1),
-                setOf(pc),
-                isRefreshing = false,
-                lastUpdatedAt = Instant.now()
-            )
-        )
-    )
+    private val internalData: HotDataFlow<Set<VaccinatedPerson>> = HotDataFlow(
+        loggingTag = TAG,
+        scope = appScope + dispatcherProvider.IO,
+        sharingBehavior = SharingStarted.Lazily,
+    ) {
+        storage.personContainers
+            .map { personContainer ->
+                VaccinatedPerson(
+                    data = personContainer,
+                    valueSet = null,
+                    isUpdatingData = false,
+                    lastError = null
+                )
+            }
+            .toSet()
+            .also { Timber.tag(TAG).v("Restored vaccination data: %s", it) }
+    }
+
+    init {
+        internalData.data
+            .onStart { Timber.tag(TAG).d("Observing test data.") }
+            .onEach { vaccinatedPersons ->
+                Timber.tag(TAG).v("Vaccination data changed: %s", vaccinatedPersons)
+                storage.personContainers = vaccinatedPersons.map { it.data }.toSet()
+            }
+            .catch {
+                it.reportProblem(TAG, "Failed to snapshot vaccination data to storage.")
+                throw it
+            }
+            .launchIn(appScope + dispatcherProvider.IO)
+    }
+
+    val vaccinationInfos: Flow<Set<VaccinatedPerson>> = combine(
+        internalData.data,
+        valueSetsRepository.latestValueSet
+    ) { personDatas, currentValueSet ->
+        personDatas.map { it.copy(valueSet = currentValueSet) }.toSet()
+    }
 
     suspend fun registerVaccination(
         qrCode: VaccinationCertificateQRCode
     ): VaccinationCertificate {
+        Timber.tag(TAG).v("registerVaccination(qrCode=%s)", qrCode)
+
+        val updatedData = internalData.updateBlocking {
+            val originalPerson = if (this.isNotEmpty()) {
+                Timber.tag(TAG).d("There is an existing person we must match.")
+                this.single().also {
+                    it.identifier.requireMatch(qrCode.personIdentifier)
+                    Timber.tag(TAG).i("New certificate matches existing person!")
+                }
+            } else {
+                VaccinatedPerson(
+                    data = PersonData(
+                        vaccinations = emptySet(),
+                        proofs = emptySet()
+                    ),
+                    valueSet = null,
+                )
+            }
+
+            val newCertificate = qrCode.toVaccinationContainer(scannedAt = timeStamper.nowUTC)
+
+            val modifiedPerson = originalPerson.copy(
+                data = originalPerson.data.copy(
+                    vaccinations = originalPerson.data.vaccinations.plus(newCertificate)
+                )
+            )
+
+            this.toMutableSet().apply {
+                remove(originalPerson)
+                add(modifiedPerson)
+            }
+        }
+
+        val updatedPerson = updatedData.single { it.identifier == qrCode.personIdentifier }
+
+        if (updatedPerson.isEligbleForProofCertificate) {
+            Timber.tag(TAG).i("%s is eligble for proof certificate, launching async check.", updatedPerson.identifier)
+            appScope.launch {
+                refresh(updatedPerson.identifier)
+            }
+        }
+
+        return updatedPerson.vaccinationCertificates.single {
+            it.certificateId == qrCode.uniqueCertificateIdentifier
+        }
+    }
+
+    suspend fun checkForProof(personIdentifier: VaccinatedPersonIdentifier?) {
+        Timber.tag(TAG).i("checkForProof(personIdentifier=%s)", personIdentifier)
+        withContext(appScope.coroutineContext) {
+            internalData.updateBlocking {
+                val originalPerson = this.singleOrNull {
+                    it.identifier == personIdentifier
+                } ?: throw VaccinatedPersonNotFoundException("Identifier=$personIdentifier")
+
+                val eligbleCert = originalPerson.data.vaccinations.first { it.isEligbleForProofCertificate }
+
+                val proof = try {
+                    vaccinationProofServer.getProofCertificate(eligbleCert.vaccinationCertificateCOSE)
+                } catch (e: Exception) {
+                    Timber.tag(TAG).e(e, "Failed to check for proof.")
+                    null
+                }
+
+                val modifiedPerson = proof?.let {
+                    originalPerson.copy(
+                        data = originalPerson.data.copy(
+                            proofs = setOf(it.toProofContainer(timeStamper.nowUTC))
+                        )
+                    )
+                } ?: originalPerson
+
+                this.toMutableSet().apply {
+                    remove(originalPerson)
+                    add(modifiedPerson)
+                }
+            }
+        }
         throw NotImplementedError()
     }
 
+    suspend fun refresh(personIdentifier: VaccinatedPersonIdentifier?) {
+        Timber.tag(TAG).d("refresh(personIdentifier=%s)", personIdentifier)
+        // TODO
+    }
+
     suspend fun clear() {
-        throw NotImplementedError()
+        Timber.tag(TAG).w("Clearing vaccination data.")
+        internalData.updateBlocking {
+            Timber.tag(TAG).v("Deleting: %s", this)
+            emptySet()
+        }
     }
 
-    suspend fun deleteVaccinationCertificate(vaccinationCertificateId: String) =
-        scope.launch {
-            // TODO delete Vaccination
+    suspend fun deleteVaccinationCertificate(vaccinationCertificateId: String) {
+        Timber.tag(TAG).w("deleteVaccinationCertificate(certificateId=%s)", vaccinationCertificateId)
+        internalData.updateBlocking {
+            val target = this.find { person ->
+                person.vaccinationCertificates.any { it.certificateId == vaccinationCertificateId }
+            } ?: throw VaccinationCertificateNotFoundException(
+                "No vaccination certificate found for $vaccinationCertificateId"
+            )
+
+            val newTarget = target.copy(
+                data = target.data.copy(
+                    vaccinations = target.data.vaccinations.filter {
+                        it.certificateId != vaccinationCertificateId
+                    }.toSet()
+                )
+            )
+
+            this.map {
+                if (it != target) newTarget else it
+            }.toSet()
         }
+    }
+
+    companion object {
+        private const val TAG = "VaccinationRepository"
+    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepository.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7ffaf2e5348106232304541a11db4fcce9562c5e
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepository.kt
@@ -0,0 +1,16 @@
+package de.rki.coronawarnapp.vaccination.core.repository
+
+import de.rki.coronawarnapp.submission.server.SubmissionServer
+import de.rki.coronawarnapp.vaccination.core.server.VaccinationValueSet
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ValueSetsRepository @Inject constructor(
+    private val submissionServer: SubmissionServer
+) {
+
+    val latestValueSet: Flow<VaccinationValueSet?> = flowOf(null)
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinatedPersonNotFoundException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinatedPersonNotFoundException.kt
new file mode 100644
index 0000000000000000000000000000000000000000..34e54c6f088361b3679092e07e728ca63d970a28
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinatedPersonNotFoundException.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.vaccination.core.repository.errors
+
+import de.rki.coronawarnapp.vaccination.core.VaccinationException
+
+class VaccinatedPersonNotFoundException(
+    message: String
+) : VaccinationException(
+    message = message,
+    cause = null
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationCertificateNotFoundException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationCertificateNotFoundException.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2b3562f9093a0d15fe7e7c812d0de28f299ac056
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationCertificateNotFoundException.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.vaccination.core.repository.errors
+
+import de.rki.coronawarnapp.vaccination.core.VaccinationException
+
+class VaccinationCertificateNotFoundException(
+    message: String
+) : VaccinationException(
+    message = message,
+    cause = null
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationDateOfBirthMissmatchException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationDateOfBirthMissmatchException.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bedc695d22726250f93bcb4273b84dc17fc21430
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationDateOfBirthMissmatchException.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.vaccination.core.repository.errors
+
+import de.rki.coronawarnapp.vaccination.core.VaccinationException
+
+class VaccinationDateOfBirthMissmatchException(
+    message: String
+) : VaccinationException(
+    message = message,
+    cause = null
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationNameMissmatchException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationNameMissmatchException.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3e27e652d6519f83fa9473c2a0e064c4b77e1411
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/errors/VaccinationNameMissmatchException.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.vaccination.core.repository.errors
+
+import de.rki.coronawarnapp.vaccination.core.VaccinationException
+
+class VaccinationNameMissmatchException(
+    message: String
+) : VaccinationException(
+    message = message,
+    cause = null
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/PersonData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/PersonData.kt
new file mode 100644
index 0000000000000000000000000000000000000000..527f570ce6f7538bea563fd77e4739ef735fbaab
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/PersonData.kt
@@ -0,0 +1,18 @@
+package de.rki.coronawarnapp.vaccination.core.repository.storage
+
+import com.google.gson.annotations.SerializedName
+import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier
+import org.joda.time.Instant
+
+data class PersonData(
+    @SerializedName("vaccinationData") val vaccinations: Set<VaccinationContainer>,
+    @SerializedName("proofData") val proofs: Set<ProofContainer>,
+    @SerializedName("lastSuccessfulProofCertificateRun") val lastSuccessfulPCRunAt: Instant = Instant.EPOCH,
+    @SerializedName("proofCertificateRunPending") val isPCRunPending: Boolean = false,
+) {
+    val identifier: VaccinatedPersonIdentifier
+        get() = vaccinations.first().personIdentifier
+
+    val isEligbleForProofCertificate: Boolean
+        get() = vaccinations.any { it.isEligbleForProofCertificate }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ProofContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ProofContainer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fec60be5d44adc3971a72a13f2acf3927020e955
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ProofContainer.kt
@@ -0,0 +1,84 @@
+package de.rki.coronawarnapp.vaccination.core.repository.storage
+
+import androidx.annotation.Keep
+import com.google.gson.annotations.SerializedName
+import de.rki.coronawarnapp.vaccination.core.ProofCertificate
+import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier
+import de.rki.coronawarnapp.vaccination.core.personIdentifier
+import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateCOSEParser
+import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateData
+import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateResponse
+import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateV1
+import de.rki.coronawarnapp.vaccination.core.server.VaccinationValueSet
+import okio.ByteString
+import org.joda.time.Instant
+import org.joda.time.LocalDate
+
+@Keep
+data class ProofContainer(
+    @SerializedName("proofCOSE") val proofCOSE: ByteString,
+    @SerializedName("receivedAt") val receivedAt: Instant,
+) {
+    @Transient internal var preParsedData: ProofCertificateData? = null
+
+    // Otherwise GSON unsafes reflection to create this class, and sets the LAZY to null
+    @Suppress("unused")
+    constructor() : this(ByteString.EMPTY, Instant.EPOCH)
+
+    @delegate:Transient
+    private val proofData: ProofCertificateData by lazy {
+        preParsedData ?: ProofCertificateCOSEParser().parse(proofCOSE)
+    }
+
+    val proof: ProofCertificateV1
+        get() = proofData.proofCertificate
+
+    val vaccination: ProofCertificateV1.VaccinationData
+        get() = proof.vaccinationDatas.single()
+
+    val personIdentifier: VaccinatedPersonIdentifier
+        get() = proof.personIdentifier
+
+    fun toProofCertificate(valueSet: VaccinationValueSet?): ProofCertificate = object : ProofCertificate {
+        override val expiresAt: Instant
+            get() = proofData.expiresAt
+
+        override val personIdentifier: VaccinatedPersonIdentifier
+            get() = proof.personIdentifier
+
+        override val firstName: String?
+            get() = proof.nameData.givenName
+        override val lastName: String
+            get() = proof.nameData.familyName ?: proof.nameData.familyNameStandardized
+        override val dateOfBirth: LocalDate
+            get() = proof.dateOfBirth
+
+        override val vaccinatedAt: LocalDate
+            get() = vaccination.vaccinatedAt
+
+        override val doseNumber: Int
+            get() = vaccination.doseNumber
+        override val totalSeriesOfDoses: Int
+            get() = vaccination.totalSeriesOfDoses
+
+        override val vaccineName: String
+            get() = valueSet?.getDisplayText(vaccination.vaccineId) ?: vaccination.vaccineId
+        override val vaccineManufacturer: String
+            get() = valueSet?.getDisplayText(vaccination.marketAuthorizationHolderId)
+                ?: vaccination.marketAuthorizationHolderId
+        override val medicalProductName: String
+            get() = valueSet?.getDisplayText(vaccination.medicalProductId) ?: vaccination.medicalProductId
+
+        override val certificateIssuer: String
+            get() = vaccination.certificateIssuer
+        override val certificateId: String
+            get() = vaccination.uniqueCertificateIdentifier
+    }
+}
+
+fun ProofCertificateResponse.toProofContainer(receivedAt: Instant) = ProofContainer(
+    proofCOSE = proofCertificateCOSE,
+    receivedAt = receivedAt,
+).apply {
+    preParsedData = proofCertificateData
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0519e17300c05a76cc42e22cdc853c9fb826fe1a
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt
@@ -0,0 +1,92 @@
+package de.rki.coronawarnapp.vaccination.core.repository.storage
+
+import androidx.annotation.Keep
+import com.google.gson.annotations.SerializedName
+import de.rki.coronawarnapp.ui.Country
+import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier
+import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate
+import de.rki.coronawarnapp.vaccination.core.personIdentifier
+import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateCOSEParser
+import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateData
+import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode
+import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateV1
+import de.rki.coronawarnapp.vaccination.core.server.VaccinationValueSet
+import okio.ByteString
+import org.joda.time.Instant
+import org.joda.time.LocalDate
+
+@Keep
+data class VaccinationContainer(
+    @SerializedName("vaccinationCertificateCOSE") val vaccinationCertificateCOSE: ByteString,
+    @SerializedName("scannedAt") val scannedAt: Instant,
+) {
+
+    @Transient internal var preParsedData: VaccinationCertificateData? = null
+
+    // Otherwise GSON unsafes reflection to create this class, and sets the LAZY to null
+    @Suppress("unused")
+    constructor() : this(ByteString.EMPTY, Instant.EPOCH)
+
+    @delegate:Transient
+    private val certificateData: VaccinationCertificateData by lazy {
+        preParsedData ?: VaccinationCertificateCOSEParser().parse(vaccinationCertificateCOSE)
+    }
+
+    val certificate: VaccinationCertificateV1
+        get() = certificateData.vaccinationCertificate
+
+    val vaccination: VaccinationCertificateV1.VaccinationData
+        get() = certificate.vaccinationDatas.single()
+
+    val certificateId: String
+        get() = vaccination.uniqueCertificateIdentifier
+
+    val personIdentifier: VaccinatedPersonIdentifier
+        get() = certificate.personIdentifier
+
+    val isEligbleForProofCertificate: Boolean
+        get() = vaccination.doseNumber == vaccination.totalSeriesOfDoses
+
+    fun toVaccinationCertificate(valueSet: VaccinationValueSet?) = object : VaccinationCertificate {
+        override val personIdentifier: VaccinatedPersonIdentifier
+            get() = certificate.personIdentifier
+
+        override val firstName: String?
+            get() = certificate.nameData.givenName
+        override val lastName: String
+            get() = certificate.nameData.familyName ?: certificate.nameData.familyNameStandardized
+
+        override val dateOfBirth: LocalDate
+            get() = certificate.dateOfBirth
+
+        override val vaccinatedAt: LocalDate
+            get() = vaccination.vaccinatedAt
+
+        override val doseNumber: Int
+            get() = vaccination.doseNumber
+        override val totalSeriesOfDoses: Int
+            get() = vaccination.totalSeriesOfDoses
+
+        override val vaccineName: String
+            get() = valueSet?.getDisplayText(vaccination.vaccineId) ?: vaccination.vaccineId
+        override val vaccineManufacturer: String
+            get() = valueSet?.getDisplayText(vaccination.marketAuthorizationHolderId)
+                ?: vaccination.marketAuthorizationHolderId
+        override val medicalProductName: String
+            get() = valueSet?.getDisplayText(vaccination.medicalProductId) ?: vaccination.medicalProductId
+
+        override val certificateIssuer: String
+            get() = vaccination.certificateIssuer
+        override val certificateCountry: Country
+            get() = Country.values().singleOrNull { it.code == vaccination.countryOfVaccination } ?: Country.DE
+        override val certificateId: String
+            get() = vaccination.uniqueCertificateIdentifier
+    }
+}
+
+fun VaccinationCertificateQRCode.toVaccinationContainer(scannedAt: Instant) = VaccinationContainer(
+    vaccinationCertificateCOSE = certificateCOSE,
+    scannedAt = scannedAt,
+).apply {
+    preParsedData = parsedData
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorage.kt
new file mode 100644
index 0000000000000000000000000000000000000000..15753b86bf2fbaa06e0431de786fea901bab1e44
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorage.kt
@@ -0,0 +1,64 @@
+package de.rki.coronawarnapp.vaccination.core.repository.storage
+
+import android.content.Context
+import androidx.core.content.edit
+import com.google.gson.Gson
+import de.rki.coronawarnapp.util.di.AppContext
+import de.rki.coronawarnapp.util.serialization.BaseGson
+import de.rki.coronawarnapp.util.serialization.fromJson
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class VaccinationStorage @Inject constructor(
+    @AppContext val context: Context,
+    @BaseGson val baseGson: Gson
+) {
+
+    private val prefs by lazy {
+        context.getSharedPreferences("vaccination_localdata", Context.MODE_PRIVATE)
+    }
+
+    private val gson by lazy {
+        // Allow for custom type adapter.
+        baseGson
+    }
+
+    var personContainers: Set<PersonData>
+        get() {
+            Timber.tag(TAG).d("vaccinatedPersons - load()")
+            val persons = prefs.all.mapNotNull { (key, value) ->
+                if (!key.startsWith(PKEY_PERSON_PREFIX)) {
+                    return@mapNotNull null
+                }
+                value as String
+                gson.fromJson<PersonData>(value).also {
+                    Timber.tag(TAG).v("Person loaded: %s", it)
+                    requireNotNull(it.identifier)
+                }
+            }
+            return persons.toSet()
+        }
+        set(persons) {
+            Timber.tag(TAG).d("vaccinatedPersons - save(%s)", persons)
+
+            prefs.edit {
+                prefs.all.keys.filter { it.startsWith(PKEY_PERSON_PREFIX) }.forEach {
+                    Timber.tag(TAG).v("Removing data for %s", it)
+                    remove(it)
+                }
+                persons.forEach {
+                    val raw = gson.toJson(it)
+                    val identifier = it.identifier
+                    Timber.tag(TAG).v("Storing vaccinatedPerson %s -> %s", identifier, raw)
+                    putString("$PKEY_PERSON_PREFIX${identifier.code}", raw)
+                }
+            }
+        }
+
+    companion object {
+        private const val TAG = "VaccinationStorage"
+        private const val PKEY_PERSON_PREFIX = "vaccination.person."
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateCOSEParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateCOSEParser.kt
new file mode 100644
index 0000000000000000000000000000000000000000..97eafeda40e53f915ea00909d2f3b79e18e8077f
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateCOSEParser.kt
@@ -0,0 +1,42 @@
+package de.rki.coronawarnapp.vaccination.core.server
+
+import okio.ByteString
+import org.joda.time.Instant
+import org.joda.time.LocalDate
+
+class ProofCertificateCOSEParser {
+
+    fun parse(proofCOSE: ByteString): ProofCertificateData {
+        // TODO
+        val cert = ProofCertificateV1(
+            version = "1.0.0",
+            nameData = ProofCertificateV1.NameData(
+                givenName = "François-Joan",
+                givenNameStandardized = "FRANCOIS<JOAN",
+                familyName = "d'Arsøns - van Halen",
+                familyNameStandardized = "DARSONS<VAN<HALEN",
+            ),
+            dateOfBirth = LocalDate.parse("2009-02-28"),
+            vaccinationDatas = listOf(
+                ProofCertificateV1.VaccinationData(
+                    targetId = "840539006",
+                    vaccineId = "1119349007",
+                    medicalProductId = "EU/1/20/1528",
+                    marketAuthorizationHolderId = "ORG-100030215",
+                    doseNumber = 2,
+                    totalSeriesOfDoses = 2,
+                    vaccinatedAt = LocalDate.parse("2021-04-22"),
+                    countryOfVaccination = "DE",
+                    certificateIssuer = "Ministry of Public Health, Welfare and Sport",
+                    uniqueCertificateIdentifier = "urn:uvci:01:NL:THECAKEISALIE",
+                )
+            )
+        )
+        return ProofCertificateData(
+            proofCertificate = cert,
+            issuedAt = Instant.EPOCH,
+            issuerCountryCode = "DE",
+            expiresAt = Instant.EPOCH
+        )
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateData.kt
new file mode 100644
index 0000000000000000000000000000000000000000..382dbc8d37a6189fc195035123c3e1a6f85c3adf
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateData.kt
@@ -0,0 +1,17 @@
+package de.rki.coronawarnapp.vaccination.core.server
+
+import org.joda.time.Instant
+
+/**
+ * Represents the information gained from data in COSE representation
+ */
+data class ProofCertificateData constructor(
+    // Parsed json
+    val proofCertificate: ProofCertificateV1,
+    // Issuer (2-letter country code)
+    val issuerCountryCode: String,
+    // Issued at (server data returns UNIX timestamp in seconds)
+    val issuedAt: Instant,
+    // Expiration time (server data returns UNIX timestamp in seconds)
+    val expiresAt: Instant,
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateException.kt
new file mode 100644
index 0000000000000000000000000000000000000000..447790e02173bdb25da275124eb56b65c913e9c0
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateException.kt
@@ -0,0 +1,11 @@
+package de.rki.coronawarnapp.vaccination.core.server
+
+import de.rki.coronawarnapp.vaccination.core.VaccinationException
+
+open class ProofCertificateException(
+    cause: Throwable?,
+    message: String
+) : VaccinationException(
+    message = message,
+    cause = cause
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateResponse.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateResponse.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bc1ab87a8b3f8a8e25569c67ce7cb4115e7a97a5
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateResponse.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.vaccination.core.server
+
+import okio.ByteString
+
+interface ProofCertificateResponse {
+    val proofCertificateData: ProofCertificateData
+
+    // COSE representation of the Proof Certificate (as byte sequence)
+    val proofCertificateCOSE: ByteString
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateServerData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateServerData.kt
deleted file mode 100644
index f298c33287488bab4704e00cfda3570da5b50271..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateServerData.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package de.rki.coronawarnapp.vaccination.core.server
-
-interface ProofCertificateServerData
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateV1.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0808cdbd70c310ad988fa034857b603fad4aa790
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/ProofCertificateV1.kt
@@ -0,0 +1,43 @@
+package de.rki.coronawarnapp.vaccination.core.server
+
+import com.google.gson.annotations.SerializedName
+import org.joda.time.LocalDate
+
+// TODO check correctness, copy paste from vaccination cert
+data class ProofCertificateV1(
+    @SerializedName("ver") val version: String,
+    @SerializedName("nam") val nameData: NameData,
+    @SerializedName("dob") val dateOfBirth: LocalDate,
+    @SerializedName("v") val vaccinationDatas: List<VaccinationData>,
+) {
+
+    data class NameData(
+        @SerializedName("fn") val familyName: String?,
+        @SerializedName("fnt") val familyNameStandardized: String,
+        @SerializedName("gn") val givenName: String?,
+        @SerializedName("gnt") val givenNameStandardized: String?,
+    )
+
+    data class VaccinationData(
+        // Disease or agent targeted, e.g. "tg": "840539006"
+        @SerializedName("tg") val targetId: String,
+        // Vaccine or prophylaxis, e.g. "vp": "1119349007"
+        @SerializedName("vp") val vaccineId: String,
+        // Vaccine medicinal product,e.g. "mp": "EU/1/20/1528",
+        @SerializedName("mp") val medicalProductId: String,
+        // Marketing Authorization Holder, e.g. "ma": "ORG-100030215",
+        @SerializedName("ma") val marketAuthorizationHolderId: String,
+        // Dose Number, e.g. "dn": 2
+        @SerializedName("dn") val doseNumber: Int,
+        // Total Series of Doses, e.g. "sd": 2,
+        @SerializedName("sd") val totalSeriesOfDoses: Int,
+        // Date of Vaccination, e.g. "dt" : "2021-04-21"
+        @SerializedName("dt") val vaccinatedAt: LocalDate,
+        // Country of Vaccination, e.g. "co": "NL"
+        @SerializedName("co") val countryOfVaccination: String,
+        // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport",
+        @SerializedName("is") val certificateIssuer: String,
+        // Unique Certificate Identifier, e.g.  "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ"
+        @SerializedName("ci") val uniqueCertificateIdentifier: String
+    )
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationProofServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationProofServer.kt
index 8259e4855f9567628a75dd586470d6f83b2b4098..dd3a546794c27e8fcf0a258ef032cc564b0de142 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationProofServer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationProofServer.kt
@@ -1,15 +1,18 @@
 package de.rki.coronawarnapp.vaccination.core.server
 
-import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate
+import dagger.Reusable
+import okio.ByteString
+import javax.inject.Inject
 
 /**
  * Talks with IBM servers?
  */
-class VaccinationProofServer {
+@Reusable
+class VaccinationProofServer @Inject constructor() {
 
     suspend fun getProofCertificate(
-        vaccinationCertificate: Set<VaccinationCertificate>
-    ): ProofCertificateServerData {
+        vaccinationCertificate: ByteString
+    ): ProofCertificateResponse {
         throw NotImplementedError()
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationServer.kt
index 4f2c5e9fc53ac18b3b4d5dc7d269f7837212b93f..3b4ed12919eec83292dbd27d63cbecabb23e245b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationServer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationServer.kt
@@ -1,11 +1,15 @@
 package de.rki.coronawarnapp.vaccination.core.server
 
+import dagger.Reusable
 import java.util.Locale
+import javax.inject.Inject
 
 /**
  * Talks with CWA servers
  */
-class VaccinationServer {
+@Reusable
+class VaccinationServer @Inject constructor() {
+
     suspend fun getVaccinationValueSets(languageCode: Locale): VaccinationValueSet {
         throw NotImplementedError()
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationValueSet.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationValueSet.kt
index f8c9d6d923ae637fe079c2a6c0aecc35a37c49a0..c1f88db7386d45b9a6e7a1ed50fbbf361fc22606 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationValueSet.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/VaccinationValueSet.kt
@@ -4,4 +4,6 @@ import java.util.Locale
 
 interface VaccinationValueSet {
     val languageCode: Locale
+
+    fun getDisplayText(key: String): String?
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/details/VaccinationDetailsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/details/VaccinationDetailsFragment.kt
index c413060ae32e7d4b499f16b91c61ef491a4f70fd..eb7be530e7fe4dce45c176839ce921b699a9c292 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/details/VaccinationDetailsFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/details/VaccinationDetailsFragment.kt
@@ -87,7 +87,7 @@ class VaccinationDetailsFragment : Fragment(R.layout.fragment_vaccination_detail
         vaccinatedAt.text = certificate.vaccinatedAt.toString(format)
         vaccineName.text = certificate.vaccineName
         vaccineManufacturer.text = certificate.vaccineManufacturer
-        chargeId.text = certificate.chargeId
+        chargeId.text = "Removed?"
         certificateIssuer.text = certificate.certificateIssuer
         certificateCountry.text = certificate.certificateCountry.getLabel(requireContext())
         certificateId.text = certificate.certificateId
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a8f8596ee00e7be776b987a6602f9ac8c8de5804
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt
@@ -0,0 +1,196 @@
+package de.rki.coronawarnapp.vaccination.core
+
+import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateData
+import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode
+import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateV1
+import de.rki.coronawarnapp.vaccination.core.repository.storage.PersonData
+import de.rki.coronawarnapp.vaccination.core.repository.storage.ProofContainer
+import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationContainer
+import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateData
+import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateResponse
+import de.rki.coronawarnapp.vaccination.core.server.ProofCertificateV1
+import okio.ByteString
+import okio.ByteString.Companion.decodeBase64
+import org.joda.time.Instant
+import org.joda.time.LocalDate
+
+object VaccinationTestData {
+
+    val PERSON_A_VAC_1_JSON = VaccinationCertificateV1(
+        version = "1.0.0",
+        nameData = VaccinationCertificateV1.NameData(
+            givenName = "François-Joan",
+            givenNameStandardized = "FRANCOIS<JOAN",
+            familyName = "d'Arsøns - van Halen",
+            familyNameStandardized = "DARSONS<VAN<HALEN",
+        ),
+        dateOfBirth = LocalDate.parse("2009-02-28"),
+        vaccinationDatas = listOf(
+            VaccinationCertificateV1.VaccinationData(
+                targetId = "840539006",
+                vaccineId = "1119349007",
+                medicalProductId = "EU/1/20/1528",
+                marketAuthorizationHolderId = "ORG-100030215",
+                doseNumber = 1,
+                totalSeriesOfDoses = 2,
+                vaccinatedAt = LocalDate.parse("2021-04-21"),
+                countryOfVaccination = "NL",
+                certificateIssuer = "Ministry of Public Health, Welfare and Sport",
+                uniqueCertificateIdentifier = "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ",
+            )
+        ),
+    )
+
+    val PERSON_A_VAC_1_DATA = VaccinationCertificateData(
+        vaccinationCertificate = PERSON_A_VAC_1_JSON
+    )
+
+    val PERSON_A_VAC_1_QRCODE = VaccinationCertificateQRCode(
+        parsedData = PERSON_A_VAC_1_DATA,
+        certificateCOSE = "VGhlIENha2UgaXMgTm90IGEgTGll".decodeBase64()!!
+    )
+
+    val PERSON_A_VAC_1_CONTAINER = VaccinationContainer(
+        scannedAt = Instant.ofEpochMilli(1620062834471),
+        vaccinationCertificateCOSE = "VGhlIGNha2UgaXMgYSBsaWUu".decodeBase64()!!,
+    ).apply {
+        preParsedData = PERSON_A_VAC_1_DATA
+    }
+
+    val PERSON_A_VAC_2_JSON = VaccinationCertificateV1(
+        version = "1.0.0",
+        nameData = VaccinationCertificateV1.NameData(
+            givenName = "François-Joan",
+            givenNameStandardized = "FRANCOIS<JOAN",
+            familyName = "d'Arsøns - van Halen",
+            familyNameStandardized = "DARSONS<VAN<HALEN",
+        ),
+        dateOfBirth = LocalDate.parse("2009-02-28"),
+        vaccinationDatas = listOf(
+            VaccinationCertificateV1.VaccinationData(
+                targetId = "840539006",
+                vaccineId = "1119349007",
+                medicalProductId = "EU/1/20/1528",
+                marketAuthorizationHolderId = "ORG-100030215",
+                doseNumber = 1,
+                totalSeriesOfDoses = 2,
+                vaccinatedAt = LocalDate.parse("2021-04-22"),
+                countryOfVaccination = "NL",
+                certificateIssuer = "Ministry of Public Health, Welfare and Sport",
+                uniqueCertificateIdentifier = "urn:uvci:01:NL:THECAKEISALIE",
+            )
+        ),
+    )
+
+    val PERSON_A_VAC_2_DATA = VaccinationCertificateData(
+        vaccinationCertificate = PERSON_A_VAC_2_JSON
+    )
+
+    val PERSON_A_VAC_2_QRCODE = VaccinationCertificateQRCode(
+        parsedData = PERSON_A_VAC_2_DATA,
+        certificateCOSE = "VGhlIGNha2UgaXMgYSBsaWUu".decodeBase64()!!
+    )
+
+    val PERSON_A_VAC_2_CONTAINER = VaccinationContainer(
+        scannedAt = Instant.ofEpochMilli(1620149234473),
+        vaccinationCertificateCOSE = "VGhlIENha2UgaXMgTm90IGEgTGll".decodeBase64()!!,
+    ).apply {
+        preParsedData = PERSON_A_VAC_2_DATA
+    }
+
+    val PERSON_A_PROOF_JSON = ProofCertificateV1(
+        version = "1.0.0",
+        nameData = ProofCertificateV1.NameData(
+            givenName = "François-Joan",
+            givenNameStandardized = "FRANCOIS<JOAN",
+            familyName = "d'Arsøns - van Halen",
+            familyNameStandardized = "DARSONS<VAN<HALEN",
+        ),
+        dateOfBirth = LocalDate.parse("2009-02-28"),
+        vaccinationDatas = listOf(
+            ProofCertificateV1.VaccinationData(
+                targetId = "840539006",
+                vaccineId = "1119349007",
+                medicalProductId = "EU/1/20/1528",
+                marketAuthorizationHolderId = "ORG-100030215",
+                doseNumber = 2,
+                totalSeriesOfDoses = 2,
+                vaccinatedAt = LocalDate.parse("2021-04-22"),
+                countryOfVaccination = "DE",
+                certificateIssuer = "Ministry of Public Health, Welfare and Sport",
+                uniqueCertificateIdentifier = "urn:uvci:01:NL:THECAKEISALIE",
+            )
+        )
+    )
+
+    val PERSON_A_PROOF_DATA = ProofCertificateData(
+        proofCertificate = PERSON_A_PROOF_JSON,
+        issuedAt = Instant.EPOCH,
+        issuerCountryCode = "DE",
+        expiresAt = Instant.EPOCH
+    )
+
+    val PERSON_A_PROOF_1_CONTAINER = ProofContainer(
+        receivedAt = Instant.ofEpochMilli(1620062834474),
+        proofCOSE = "VGhpc0lzQVByb29mQ09TRQ".decodeBase64()!!,
+    ).apply {
+        preParsedData = PERSON_A_PROOF_DATA
+    }
+
+    val PERSON_A_PROOF_1_RESPONSE = object : ProofCertificateResponse {
+        override val proofCertificateData: ProofCertificateData
+            get() = ProofCertificateData(
+                proofCertificate = PERSON_A_PROOF_JSON,
+                expiresAt = Instant.EPOCH,
+                issuedAt = Instant.EPOCH,
+                issuerCountryCode = "DE",
+            )
+        override val proofCertificateCOSE: ByteString
+            get() = "VGhpc0lzQVByb29mQ09TRQ".decodeBase64()!!
+    }
+
+    val PERSON_A_DATA_2VAC_PROOF = PersonData(
+        vaccinations = setOf(PERSON_A_VAC_1_CONTAINER, PERSON_A_VAC_2_CONTAINER),
+        proofs = setOf(PERSON_A_PROOF_1_CONTAINER),
+    )
+
+    val PERSON_B_VAC_1_JSON = VaccinationCertificateV1(
+        version = "1.0.0",
+        nameData = VaccinationCertificateV1.NameData(
+            givenName = "Sir Jakob",
+            givenNameStandardized = "SIR<JAKOB",
+            familyName = "Von Mustermensch",
+            familyNameStandardized = "VON<MUSTERMENSCH",
+        ),
+        dateOfBirth = LocalDate.parse("1996-12-24"),
+        vaccinationDatas = listOf(
+            VaccinationCertificateV1.VaccinationData(
+                targetId = "840539006",
+                vaccineId = "1119349007",
+                medicalProductId = "EU/1/20/1528",
+                marketAuthorizationHolderId = "ORG-100030215",
+                doseNumber = 1,
+                totalSeriesOfDoses = 2,
+                vaccinatedAt = LocalDate.parse("2021-04-21"),
+                countryOfVaccination = "NL",
+                certificateIssuer = "Ministry of Public Health, Welfare and Sport",
+                uniqueCertificateIdentifier = "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ",
+            )
+        )
+    )
+    val PERSON_B_VAC_1_DATA = VaccinationCertificateData(
+        vaccinationCertificate = PERSON_B_VAC_1_JSON
+    )
+
+    val PERSON_B_VAC_1_CONTAINER = VaccinationContainer(
+        scannedAt = Instant.ofEpochMilli(1620062834471),
+        vaccinationCertificateCOSE = "VGhpc0lzSmFrb2I".decodeBase64()!!,
+    ).apply {
+        preParsedData = PERSON_B_VAC_1_DATA
+    }
+
+    val PERSON_B_DATA_1VAC_NOPROOF = PersonData(
+        vaccinations = setOf(PERSON_B_VAC_1_CONTAINER),
+        proofs = emptySet()
+    )
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9a707688ee09916569fac6caa90e9828045a41d4
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt
@@ -0,0 +1,135 @@
+package de.rki.coronawarnapp.vaccination.core.repository
+
+import de.rki.coronawarnapp.util.TimeStamper
+import de.rki.coronawarnapp.vaccination.core.VaccinationTestData
+import de.rki.coronawarnapp.vaccination.core.repository.storage.PersonData
+import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationStorage
+import de.rki.coronawarnapp.vaccination.core.server.VaccinationProofServer
+import de.rki.coronawarnapp.vaccination.core.server.VaccinationValueSet
+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.CoroutineScope
+import kotlinx.coroutines.flow.flowOf
+import org.joda.time.Instant
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import testhelpers.TestDispatcherProvider
+import testhelpers.coroutines.runBlockingTest2
+import timber.log.Timber
+
+class VaccinationRepositoryTest : BaseTest() {
+
+    @MockK lateinit var timeStamper: TimeStamper
+
+    @MockK lateinit var storage: VaccinationStorage
+    @MockK lateinit var valueSetsRepository: ValueSetsRepository
+    @MockK lateinit var vaccinationProofServer: VaccinationProofServer
+    @MockK lateinit var vaccinationValueSet: VaccinationValueSet
+
+    private var testStorage: Set<PersonData> = emptySet()
+
+    private var nowUTC = Instant.ofEpochMilli(1234567890)
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        every { timeStamper.nowUTC } returns nowUTC
+
+        every { valueSetsRepository.latestValueSet } returns flowOf(vaccinationValueSet)
+
+        coEvery { vaccinationProofServer.getProofCertificate(any()) } returns VaccinationTestData.PERSON_A_PROOF_1_RESPONSE
+
+        storage.apply {
+            every { personContainers } answers { testStorage }
+            every { personContainers = any() } answers { testStorage = arg(0) }
+        }
+    }
+
+    private fun createInstance(scope: CoroutineScope) = VaccinationRepository(
+        appScope = scope,
+        dispatcherProvider = TestDispatcherProvider(),
+        timeStamper = timeStamper,
+        storage = storage,
+        valueSetsRepository = valueSetsRepository,
+        vaccinationProofServer = vaccinationProofServer,
+    )
+
+    @Test
+    fun `add new certificate - no prior data`() = runBlockingTest2(ignoreActive = true) {
+        val instance = createInstance(this)
+
+        advanceUntilIdle()
+
+        instance.registerVaccination(VaccinationTestData.PERSON_A_VAC_1_QRCODE).apply {
+            Timber.i("Returned cert is %s", this)
+            this.personIdentifier shouldBe VaccinationTestData.PERSON_A_VAC_1_CONTAINER.personIdentifier
+        }
+    }
+
+    @Test
+    fun `add new certificate - existing data`() = runBlockingTest2(ignoreActive = true) {
+//        val dataBefore = VaccinationTestData.PERSON_A_DATA_2VAC_PROOF.copy(
+//            vaccinations = setOf(VaccinationTestData.PERSON_A_VAC_1_CONTAINER),
+//            proofs = emptySet()
+//        )
+//        val dataAfter = VaccinationTestData.PERSON_A_DATA_2VAC_PROOF.copy(
+//            vaccinations = setOf(
+//                VaccinationTestData.PERSON_A_VAC_1_CONTAINER,
+//                VaccinationTestData.PERSON_A_VAC_2_CONTAINER.copy(scannedAt = nowUTC)
+//            ),
+//            proofs = emptySet()
+//        )
+//        testStorage = setOf(dataBefore)
+//
+//        val instance = createInstance(this)
+//
+//        advanceUntilIdle()
+//
+//        instance.registerVaccination(VaccinationTestData.PERSON_A_VAC_2_QRCODE).apply {
+//            Timber.i("Returned cert is %s", this)
+//            this.personIdentifier shouldBe VaccinationTestData.PERSON_A_VAC_2_CONTAINER.personIdentifier
+//        }
+//
+//        testStorage.first() shouldBe dataAfter
+    }
+
+    @Test
+    fun `add new certificate - if eligble for proof, start request`() = runBlockingTest2(ignoreActive = true) {
+//        TODO()
+    }
+
+    @Test
+    fun `add new certificate - does not match existing person`() {
+//        TODO()
+    }
+
+    @Test
+    fun `add new certificate - duplicate certificate`() {
+//        TODO()
+    }
+
+    @Test
+    fun `clear data`() {
+//        TODO()
+    }
+
+    @Test
+    fun `remove certificate`() {
+//        TODO()
+    }
+
+    @Test
+    fun `remove certificate - starts proof check if we deleted a vaccination that was eligble for proof`() {
+//        TODO()
+    }
+
+    @Test
+    fun `check for new proof certificate`() {
+//        TODO()
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ProofContainerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ProofContainerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3676dac8ed9c0a8379c18031d1777d1ae028a951
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ProofContainerTest.kt
@@ -0,0 +1,12 @@
+package de.rki.coronawarnapp.vaccination.core.repository.storage
+
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class ProofContainerTest : BaseTest() {
+
+    @Test
+    fun `person identifier calculation`() {
+        // TODO
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..46e3165ff8088c73ac7d6178352fafe6465826f3
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt
@@ -0,0 +1,12 @@
+package de.rki.coronawarnapp.vaccination.core.repository.storage
+
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class VaccinationContainerTest : BaseTest() {
+
+    @Test
+    fun `person identifier calculation`() {
+        // TODO
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorageTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7139897663af75b859c059e11ca2a5ac6e7303bf
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorageTest.kt
@@ -0,0 +1,143 @@
+package de.rki.coronawarnapp.vaccination.core.repository.storage
+
+import android.content.Context
+import androidx.core.content.edit
+import de.rki.coronawarnapp.util.serialization.SerializationModule
+import de.rki.coronawarnapp.vaccination.core.VaccinationTestData
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import okio.ByteString.Companion.decodeBase64
+import org.junit.Ignore
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import testhelpers.extensions.toComparableJsonPretty
+import testhelpers.preferences.MockSharedPreferences
+
+@Ignore
+class VaccinationStorageTest : BaseTest() {
+
+    @MockK lateinit var context: Context
+    private lateinit var mockPreferences: MockSharedPreferences
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        mockPreferences = MockSharedPreferences()
+
+        every {
+            context.getSharedPreferences("vaccination_localdata", Context.MODE_PRIVATE)
+        } returns mockPreferences
+    }
+
+    private fun createInstance() = VaccinationStorage(
+        context = context,
+        baseGson = SerializationModule().baseGson()
+    )
+
+    @Test
+    fun `init is sideeffect free`() {
+        createInstance()
+    }
+
+    @Test
+    fun `storing empty set deletes data`() {
+        mockPreferences.edit {
+            putString("dontdeleteme", "test")
+            putString("vaccination.person.test", "test")
+        }
+        createInstance().personContainers = emptySet()
+
+        mockPreferences.dataMapPeek.keys.single() shouldBe "dontdeleteme"
+    }
+
+    @Test
+    fun `store one fully vaccinated person`() {
+        val instance = createInstance()
+        instance.personContainers = setOf(VaccinationTestData.PERSON_A_DATA_2VAC_PROOF)
+
+        val json =
+            (mockPreferences.dataMapPeek["vaccination.person.2009-02-28#DARSONS<VAN<HALEN#FRANCOIS<JOAN"] as String)
+
+        json.toComparableJsonPretty() shouldBe """
+            {
+                "vaccinationData": [
+                    {
+                        "vaccinationCertificateCOSE": "VGhlIGNha2UgaXMgYSBsaWUu",
+                        "scannedAt": 1620062834471
+                    },
+                    {
+                        "vaccinationCertificateCOSE": "VGhlIENha2UgaXMgTm90IGEgTGll",
+                        "scannedAt": 1620149234473
+                    }
+                ],
+                "proofData": [
+                    {
+                        "proofCOSE": "VGhpc0lzQVByb29mQ09TRQ==",
+                        "receivedAt": 1620062834474
+                    }
+                ],
+                "lastSuccessfulProofCertificateRun": 0,
+                "proofCertificateRunPending": false
+            }
+        """.toComparableJsonPretty()
+
+        instance.personContainers.single().apply {
+            this shouldBe VaccinationTestData.PERSON_A_DATA_2VAC_PROOF
+            this.vaccinations.map { it.vaccinationCertificateCOSE } shouldBe setOf(
+                "VGhlIGNha2UgaXMgYSBsaWUu".decodeBase64()!!,
+                "VGhlIENha2UgaXMgTm90IGEgTGll".decodeBase64()!!,
+            )
+            this.proofs.map { it.proofCOSE } shouldBe setOf(
+                "VGhpc0lzQVByb29mQ09TRQ==".decodeBase64()!!,
+            )
+        }
+    }
+
+    @Test
+    fun `store incompletely vaccinated person`() {
+        val instance = createInstance()
+        instance.personContainers = setOf(VaccinationTestData.PERSON_B_DATA_1VAC_NOPROOF)
+
+        val json = (mockPreferences.dataMapPeek["vaccination.person.1996-12-24#VON<MUSTERMENSCH#SIR<JAKOB"] as String)
+
+        json.toComparableJsonPretty() shouldBe """
+            {
+                "vaccinationData": [
+                    {
+                        "vaccinationCertificateCOSE": "VGhpc0lzSmFrb2I=",
+                        "scannedAt": 1620062834471
+                    }
+                ],
+                "proofData": [],
+                "lastSuccessfulProofCertificateRun": 0,
+                "proofCertificateRunPending": false
+            }
+        """.toComparableJsonPretty()
+
+        instance.personContainers.single().apply {
+            this shouldBe VaccinationTestData.PERSON_B_DATA_1VAC_NOPROOF
+            this.vaccinations.single().vaccinationCertificateCOSE shouldBe "VGhpc0lzSmFrb2I=".decodeBase64()!!
+        }
+    }
+
+    @Test
+    fun `store two persons`() {
+        createInstance().apply {
+            personContainers = setOf(
+                VaccinationTestData.PERSON_B_DATA_1VAC_NOPROOF,
+                VaccinationTestData.PERSON_A_DATA_2VAC_PROOF
+            )
+            personContainers shouldBe setOf(
+                VaccinationTestData.PERSON_B_DATA_1VAC_NOPROOF,
+                VaccinationTestData.PERSON_A_DATA_2VAC_PROOF
+            )
+
+            personContainers = emptySet()
+            personContainers shouldBe emptySet()
+        }
+    }
+}