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

Vaccination Repository Part 1 (EXPOSUREAPP-6729) (#3058)

* Repository WIP

* Add vaccination location

* Adjust label and type for country.

* Vaccination storage WIP, Draft 2

* Vaccination storage WIP, Draft 5

* Vaccination storage WIP, Draft 6

* Revert gradle change.

* Vaccination storage WIP, Draft 7

* Adjust vaccination certificate json schema.

* Extend proof certificate with known attributes, adjust unit tests.

* Error handling, TODOs, more structure, storage and tests.

* LINTs

* Restructure storage approach, reparsing instead of duplicate data.

* Fix temporary issues.
parent e481f244
No related branches found
No related tags found
No related merge requests found
Showing
with 684 additions and 92 deletions
...@@ -6,9 +6,11 @@ import dagger.Module ...@@ -6,9 +6,11 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.Reusable import dagger.Reusable
import de.rki.coronawarnapp.util.serialization.adapter.ByteArrayAdapter 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.DurationAdapter
import de.rki.coronawarnapp.util.serialization.adapter.InstantAdapter import de.rki.coronawarnapp.util.serialization.adapter.InstantAdapter
import de.rki.coronawarnapp.util.serialization.adapter.LocalDateAdapter import de.rki.coronawarnapp.util.serialization.adapter.LocalDateAdapter
import okio.ByteString
import org.joda.time.Duration import org.joda.time.Duration
import org.joda.time.Instant import org.joda.time.Instant
import org.joda.time.LocalDate import org.joda.time.LocalDate
...@@ -24,5 +26,6 @@ class SerializationModule { ...@@ -24,5 +26,6 @@ class SerializationModule {
.registerTypeAdapter(LocalDate::class.java, LocalDateAdapter()) .registerTypeAdapter(LocalDate::class.java, LocalDateAdapter())
.registerTypeAdapter(Duration::class.java, DurationAdapter()) .registerTypeAdapter(Duration::class.java, DurationAdapter())
.registerTypeAdapter(ByteArray::class.java, ByteArrayAdapter()) .registerTypeAdapter(ByteArray::class.java, ByteArrayAdapter())
.registerTypeAdapter(ByteString::class.java, ByteStringBase64Adapter())
.create() .create()
} }
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")
}
}
}
package de.rki.coronawarnapp.vaccination.core package de.rki.coronawarnapp.vaccination.core
import org.joda.time.Instant import org.joda.time.Instant
import org.joda.time.LocalDate
interface ProofCertificate {
val personIdentifier: VaccinatedPersonIdentifier
data class ProofCertificate(
val expiresAt: Instant 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
}
package de.rki.coronawarnapp.vaccination.core 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 import org.joda.time.LocalDate
data class VaccinatedPerson( data class VaccinatedPerson(
val vaccinationCertificates: Set<VaccinationCertificate>, internal val data: PersonData,
val proofCertificates: Set<ProofCertificate>, private val valueSet: VaccinationValueSet?,
val isRefreshing: Boolean, val isUpdatingData: Boolean = false,
val lastUpdatedAt: Instant, val lastError: Throwable? = null,
) { ) {
val identifier: VaccinatedPersonIdentifier = "" val identifier: VaccinatedPersonIdentifier
get() = data.identifier
val firstName: String val vaccinationStatus: Status
get() = "" 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 val lastName: String
get() = "" get() = vaccinationCertificates.first().lastName
val dateOfBirth: LocalDate val dateOfBirth: LocalDate
get() = LocalDate.now() get() = vaccinationCertificates.first().dateOfBirth
val vaccinationStatus: Status val isEligbleForProofCertificate: Boolean
get() = if (proofCertificates.isNotEmpty()) Status.COMPLETE else Status.INCOMPLETE get() = data.isEligbleForProofCertificate
enum class Status { enum class Status {
INCOMPLETE, INCOMPLETE,
COMPLETE COMPLETE
} }
} }
typealias VaccinatedPersonIdentifier = String
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
package de.rki.coronawarnapp.vaccination.core package de.rki.coronawarnapp.vaccination.core
import de.rki.coronawarnapp.ui.Country import de.rki.coronawarnapp.ui.Country
import org.joda.time.Instant
import org.joda.time.LocalDate import org.joda.time.LocalDate
data class VaccinationCertificate( interface VaccinationCertificate {
val firstName: String, val firstName: String?
val lastName: String, val lastName: String
val dateOfBirth: LocalDate, val dateOfBirth: LocalDate
val vaccinatedAt: Instant, val vaccinatedAt: LocalDate
val vaccineName: String,
val vaccineManufacturer: String, val vaccineName: String
val chargeId: String, val vaccineManufacturer: String
val certificateIssuer: String, val medicalProductName: String
val certificateCountry: Country,
val certificateId: String, val doseNumber: Int
) { val totalSeriesOfDoses: Int
val identifier: VaccinatedPersonIdentifier get() = ""
val certificateIssuer: String
val certificateCountry: Country
val certificateId: String
val personIdentifier: VaccinatedPersonIdentifier
} }
package de.rki.coronawarnapp.vaccination.core
open class VaccinationException(
cause: Throwable?,
message: String
) : Exception(message, cause)
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
)
}
}
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
)
package de.rki.coronawarnapp.vaccination.core.qrcode package de.rki.coronawarnapp.vaccination.core.qrcode
import okio.ByteString
// TODO // TODO
data class VaccinationCertificateQRCode( data class VaccinationCertificateQRCode(
// Vaccine or prophylaxis val parsedData: VaccinationCertificateData,
val vaccineNameId: String, // COSE representation of the vaccination certificate (as byte sequence)
val vaccineMedicinalProduct: String, val certificateCOSE: ByteString,
val marketAuthorizationHolder: String, ) {
) val uniqueCertificateIdentifier: String
get() = parsedData.vaccinationCertificate.vaccinationDatas.single().uniqueCertificateIdentifier
}
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
)
}
package de.rki.coronawarnapp.vaccination.core.repository 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.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.VaccinatedPerson
import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier
import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate 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.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.CoroutineScope
import kotlinx.coroutines.flow.Flow 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 kotlinx.coroutines.launch
import org.joda.time.Instant import kotlinx.coroutines.plus
import org.joda.time.LocalDate import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class VaccinationRepository @Inject constructor( 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( private val internalData: HotDataFlow<Set<VaccinatedPerson>> = HotDataFlow(
firstName = "Max", loggingTag = TAG,
lastName = "Mustermann", scope = appScope + dispatcherProvider.IO,
dateOfBirth = LocalDate.now(), sharingBehavior = SharingStarted.Lazily,
vaccinatedAt = Instant.now(), ) {
vaccineName = "Comirnaty (mRNA)", storage.personContainers
vaccineManufacturer = "BioNTech", .map { personContainer ->
chargeId = "CB2342", VaccinatedPerson(
certificateIssuer = "Landratsamt Potsdam", data = personContainer,
certificateCountry = Country.DE, valueSet = null,
certificateId = "05930482748454836478695764787840" isUpdatingData = false,
) lastError = null
)
private val vc1 = VaccinationCertificate( }
firstName = "Max", .toSet()
lastName = "Mustermann", .also { Timber.tag(TAG).v("Restored vaccination data: %s", it) }
dateOfBirth = LocalDate.now(), }
vaccinatedAt = Instant.now(),
vaccineName = "Comirnaty (mRNA)", init {
vaccineManufacturer = "BioNTech", internalData.data
chargeId = "CB2342", .onStart { Timber.tag(TAG).d("Observing test data.") }
certificateIssuer = "Landratsamt Potsdam", .onEach { vaccinatedPersons ->
certificateCountry = Country.DE, Timber.tag(TAG).v("Vaccination data changed: %s", vaccinatedPersons)
certificateId = "05930482748454836478695764787841" storage.personContainers = vaccinatedPersons.map { it.data }.toSet()
) }
.catch {
private val pc = ProofCertificate( it.reportProblem(TAG, "Failed to snapshot vaccination data to storage.")
expiresAt = Instant.now() throw it
) }
.launchIn(appScope + dispatcherProvider.IO)
// TODO read from repos }
val vaccinationInfos: Flow<Set<VaccinatedPerson>> = flowOf(
setOf( val vaccinationInfos: Flow<Set<VaccinatedPerson>> = combine(
VaccinatedPerson( internalData.data,
setOf(vc), valueSetsRepository.latestValueSet
setOf(), ) { personDatas, currentValueSet ->
isRefreshing = false, personDatas.map { it.copy(valueSet = currentValueSet) }.toSet()
lastUpdatedAt = Instant.now() }
),
VaccinatedPerson(
setOf(vc1),
setOf(pc),
isRefreshing = false,
lastUpdatedAt = Instant.now()
)
)
)
suspend fun registerVaccination( suspend fun registerVaccination(
qrCode: VaccinationCertificateQRCode qrCode: VaccinationCertificateQRCode
): VaccinationCertificate { ): 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() throw NotImplementedError()
} }
suspend fun refresh(personIdentifier: VaccinatedPersonIdentifier?) {
Timber.tag(TAG).d("refresh(personIdentifier=%s)", personIdentifier)
// TODO
}
suspend fun clear() { 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) = suspend fun deleteVaccinationCertificate(vaccinationCertificateId: String) {
scope.launch { Timber.tag(TAG).w("deleteVaccinationCertificate(certificateId=%s)", vaccinationCertificateId)
// TODO delete Vaccination 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"
}
} }
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)
}
package de.rki.coronawarnapp.vaccination.core.repository.errors
import de.rki.coronawarnapp.vaccination.core.VaccinationException
class VaccinatedPersonNotFoundException(
message: String
) : VaccinationException(
message = message,
cause = null
)
package de.rki.coronawarnapp.vaccination.core.repository.errors
import de.rki.coronawarnapp.vaccination.core.VaccinationException
class VaccinationCertificateNotFoundException(
message: String
) : VaccinationException(
message = message,
cause = null
)
package de.rki.coronawarnapp.vaccination.core.repository.errors
import de.rki.coronawarnapp.vaccination.core.VaccinationException
class VaccinationDateOfBirthMissmatchException(
message: String
) : VaccinationException(
message = message,
cause = null
)
package de.rki.coronawarnapp.vaccination.core.repository.errors
import de.rki.coronawarnapp.vaccination.core.VaccinationException
class VaccinationNameMissmatchException(
message: String
) : VaccinationException(
message = message,
cause = null
)
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 }
}
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
}
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
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment