Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions NEW_SCHEMAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,32 @@ such that a JSON parser can automatically parse the incoming payload as an objec
is scoped using HAPI FHIR elements to reuse parsing, but it is likely that custom parsers will
be needed given the space limitations inside QR codes.

### ICVP Logical Models

For International Certificate of Vaccination or Prophylaxis (ICVP), we provide several logical models:

- **`DvcLogicalModel`**: Base digital vaccine certificate model with core fields (n, dob, s, ntl, nid, ndt, gn, v)
- **`IcvpLogicalModel`**: ICVP-specific model that extends DVC with ICVP validation constraints and uses `IcvpVaccineDetails`
- **`IcvpEventLogicalModel`**: Event-based ICVP model for specific vaccination events
- **`HCertDVC`**: HCert-compatible representation of the DVC model

All models now include the `ndt` (National ID Document Type) field as required by current FSH specifications.

### Key Features

- **Type Safety**: `IcvpLogicalModel` uses `IcvpVaccineDetails` instead of generic `DvcVaccineDetails` to ensure ICVP-specific validation
- **Dynamic Product Validation**:
- Loads ICVP Product IDs directly from WHO SMART guidelines PreQual CodeSystem
- HTTP client implementation to fetch from `http://smart.who.int/pcmt-vaxprequal/CodeSystem/PreQualProductIDs`
- Graceful fallback to format validation when source is unavailable
- Caching mechanism for performance optimization
- **Constraint Validation**: Built-in validation including:
- "must-have-issuer-or-clinician-name" invariant
- National ID document type validation against HL7 v2-0203
- **Cache Management**: `refreshProductIdCache()` method for testing and cache updates
- **Error Handling**: Robust error handling for network issues and malformed responses
- **Backward Compatibility**: All constructors maintain default values for existing integrations

## 4. Code the Mapper with Structure Maps

The mapper is a simple class that points to a Structure Map file in the IG. The structure map
Expand All @@ -45,12 +71,18 @@ The mapper itself is just the loader for the Structure Map.
* Translates a QR CBOR object into FHIR Objects
*/
class IcvpMapper: BaseMapper() {
fun run(ddcc: DdccLogicalModel): Bundle {
return super.run(ddcc, "ICVPtoDDCC.map")
fun run(icvp: IcvpLogicalModel): Bundle {
return super.run(icvp, "DVCLMToIPS.map")
}
}
```

### Available Structure Maps

- **`DVCLMToIPS.map`**: Converts DVC/ICVP logical models to International Patient Summary (IPS) format
- Maps `ndt` field to `Patient.identifier.type` for proper identification
- Handles ICVP-specific vaccine details with product ID validation

## 5. Adapt the interface for the new Bundle

Once the Bundle is ready the App must display its contents in a new Card. Currently the app
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ anywhere. Our goal is to make a Verifier App with the widest possible verificati
3. EU DCC, WHO DDCC and LAC PASS DCC
4. ICAO Visible Digital Seals
3. Verifies the issuer's trust using a [DID-Based](https://www.w3.org/TR/did-core/) Trust List from the [Global Digital Health Certification Network](https://www.who.int/initiatives/global-digital-health-certification-network)
4. Transform the QR Payload using [FHIR Structure Maps](https://worldhealthorganization.github.io/ddcc/) for [International
Certificate of Vaccination of Prophylaxis] (https://worldhealthorganization.github.io/smart-icvp/artifacts.html) and [International Patient Summary](https://hl7.org/fhir/uv/ips/)
4. Transform the QR Payload using [FHIR Structure Maps](https://worldhealthorganization.github.io/ddcc/) for [International Certificate of Vaccination or Prophylaxis](https://worldhealthorganization.github.io/smart-icvp/artifacts.html) and [International Patient Summary](https://hl7.org/fhir/uv/ips/)
- **ICVP Logical Models**: Full support for FSH-compliant ICVP models with enhanced validation
- **Product ID Validation**: Dynamic loading from WHO SMART PreQual CodeSystem with graceful fallback
- **Document Type Support**: Includes `ndt` (National ID Document Type) field per current specifications
5. Calculates the assessment of the health information using CQL Libraries from subscribed IGs
6. Displays the medical information, the credential information, the issuer information and the assessment results in the screen.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class HCertDVC(
val s: CodeType?, // Sex
val ntl: CodeType?, // Nationality
val nid: StringType?, // National Identification Document
val ndt: CodeType?, // National ID Document Type
val gn: StringType?, // Parent or Guardian Name

val v: DvcHCertVaccination?, // Vaccination Group (Can only have one)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ open class DvcLogicalModel(
var nationality: CodeType? = null,

var nid: StringType? = null,
var ndt: CodeType? = null, // National ID Document Type
var guardian: StringType? = null,

@JsonDeserialize(using = ReferenceDeserializer::class)
var issuer: Reference? = null,
var vaccineDetails: DvcVaccineDetails? = null,
): BaseModel() {
override fun copy(): Resource? { return DvcLogicalModel(name, dob, sex, nationality, nid, guardian, issuer, vaccineDetails) }
override fun copy(): Resource? { return DvcLogicalModel(name, dob, sex, nationality, nid, ndt, guardian, issuer, vaccineDetails) }
override fun getResourceType(): ResourceType? {
println("DvcLogicalModel GetResourceType")
return ResourceType.StructureDefinition
Expand All @@ -52,6 +53,7 @@ open class DvcLogicalModel(
"sex".hashCode() -> sex = (value as? CodeType)
"nationality".hashCode() -> nationality = (value as? CodeType)
"nid".hashCode() -> nid = (value as? StringType)
"ndt".hashCode() -> ndt = (value as? CodeType)
"guardian".hashCode() -> guardian = (value as? StringType)
"vaccineDetails".hashCode() -> vaccineDetails = (value as? DvcVaccineDetails)
else -> super.setProperty(hash, name, value)
Expand All @@ -67,6 +69,7 @@ open class DvcLogicalModel(
"sex" -> sex = (value as? CodeType)
"nationality" -> nationality = (value as? CodeType)
"nid" -> nid = (value as? StringType)
"ndt" -> ndt = (value as? CodeType)
"guardian" -> guardian = (value as? StringType)
"vaccineDetails" -> vaccineDetails = (value as? DvcVaccineDetails)
else -> super.setProperty(name, value)
Expand All @@ -77,4 +80,25 @@ open class DvcLogicalModel(
override fun fhirType(): String {
return "ModelDVC"
}

/**
* Validates ICVP constraints for the logical model
*/
fun validateIcvpConstraints(): List<String> {
val errors = mutableListOf<String>()

// Validate National ID Document Type
if (!IcvpValidation.validateNationalIdDocumentType(ndt?.value)) {
errors.add("National ID Document Type (ndt) must be a valid identifier type from v2-0203")
}

// Validate vaccine details if present
vaccineDetails?.let { details ->
if (details is DvcVaccineDetails) {
errors.addAll(details.validateIcvpConstraints())
}
}

return errors
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,20 @@ open class DvcVaccineDetails (
}
return value
}

/**
* Validates ICVP constraints for vaccine details
*/
open fun validateIcvpConstraints(): List<String> {
val errors = mutableListOf<String>()

// Validate must-have-issuer-or-clinician-name invariant
if (!IcvpValidation.validateIssuerOrClinicanName(
issuer?.reference?.let { StringType(it) },
clinicianName)) {
errors.add("Either issuer or clinicianName must be present")
}

return errors
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ open class IcvpEventLogicalModel(
nationality: CodeType?,

nid: StringType?,
ndt: CodeType?,
guardian: StringType?,

issuer: Reference?,
vaccineDetail: DvcVaccineDetails
): DvcLogicalModel(name, dob, sex, nationality, nid, guardian, issuer, vaccineDetail)
): DvcLogicalModel(name, dob, sex, nationality, nid, ndt, guardian, issuer, vaccineDetail)
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ open class IcvpLogicalModel(
nationality: CodeType?,

nid: StringType?,
ndt: CodeType?,
guardian: StringType?,

issuer: Reference?,
vaccineDetails: DvcVaccineDetails
): DvcLogicalModel(name, dob, sex, nationality, nid, guardian, issuer, vaccineDetails)
vaccineDetails: IcvpVaccineDetails
): DvcLogicalModel(name, dob, sex, nationality, nid, ndt, guardian, issuer, vaccineDetails)
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,20 @@ class IcvpVaccineDetails (

batchNo: StringType?,
validityPeriod: Period?,
): DvcVaccineDetails(doseNumber, disease, vaccineClassification, vaccineTradeItem, date, clinicianName, issuer, manufacturerId, manufacturer, batchNo, validityPeriod)
): DvcVaccineDetails(doseNumber, disease, vaccineClassification, vaccineTradeItem, date, clinicianName, issuer, manufacturerId, manufacturer, batchNo, validityPeriod) {

/**
* Validates ICVP-specific constraints including product ID constraints
*/
override fun validateIcvpConstraints(): List<String> {
val errors = super.validateIcvpConstraints().toMutableList()

// Validate is-an-icvp-product-id invariant
val productId = vaccineTradeItem?.value
if (!IcvpValidation.validateIcvpProductId(productId)) {
errors.add("Product ID must come from the ICVP vaccines from the PreQual Database")
}

return errors
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package org.who.gdhcnvalidator.verify.hcert.icvp

import org.hl7.fhir.r4.model.StringType
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import java.net.URL
import java.net.URLConnection
import java.io.IOException

/**
* Validation helpers for ICVP logical models to ensure compliance with FSH specifications
*/
object IcvpValidation {

/**
* ICVP Product ID system as defined in the FSH
*/
const val ICVP_PRODUCT_ID_SYSTEM = "http://smart.who.int/pcmt-vaxprequal/CodeSystem/PreQualProductIDs"

/**
* Cache for ICVP Product IDs loaded from the PreQual database
* This would be populated from the actual ICVP PreQual value set
*/
private var cachedProductIds: Set<String>? = null

/**
* Forces a refresh of the cached product IDs from the source
* Useful for testing or when the cache needs to be updated
*/
fun refreshProductIdCache() {
cachedProductIds = null
}

/**
* Loads ICVP Product IDs from the PreQual database
* Fetches the FHIR CodeSystem from the WHO SMART guidelines and extracts product IDs
*/
private fun loadIcvpProductIds(): Set<String> {
return try {
val url = URL(ICVP_PRODUCT_ID_SYSTEM)
val connection: URLConnection = url.openConnection()
connection.setRequestProperty("Accept", "application/fhir+json")
connection.connectTimeout = 10000 // 10 seconds
connection.readTimeout = 10000 // 10 seconds

val jsonResponse = connection.getInputStream().bufferedReader().use { it.readText() }
val objectMapper = ObjectMapper()
val jsonNode: JsonNode = objectMapper.readTree(jsonResponse)

// Extract product IDs from FHIR CodeSystem concept codes
val productIds = mutableSetOf<String>()

// Check if this is a valid FHIR CodeSystem
if (jsonNode.has("resourceType") &&
jsonNode.get("resourceType").asText() == "CodeSystem" &&
jsonNode.has("concept")) {

val concepts = jsonNode.get("concept")
if (concepts.isArray) {
for (concept in concepts) {
if (concept.has("code")) {
val code = concept.get("code").asText()
if (code.isNotBlank()) {
productIds.add(code)
}
}
}
}
}

productIds.toSet()
} catch (e: IOException) {
// Log the error but don't fail validation entirely
// In production, you might want to use proper logging here
System.err.println("Warning: Could not load ICVP Product IDs from ${ICVP_PRODUCT_ID_SYSTEM}: ${e.message}")
emptySet()
} catch (e: Exception) {
// Handle any other parsing or network errors
System.err.println("Warning: Error parsing ICVP Product IDs: ${e.message}")
emptySet()
}
}

/**
* Validates that the vaccine product ID comes from the ICVP Product Catalogue
* as required by the is-an-icvp-product-id invariant
*/
fun validateIcvpProductId(productId: String?): Boolean {
if (productId.isNullOrBlank()) return false

// Load product IDs from source if not cached
if (cachedProductIds == null) {
cachedProductIds = loadIcvpProductIds()
}

// If we successfully loaded product IDs from the source, validate against them
if (cachedProductIds!!.isNotEmpty()) {
return cachedProductIds!!.contains(productId)
}

// Fallback to format validation if source is not available
// This ensures validation doesn't completely fail if the WHO server is down
return productId.isNotBlank() &&
(productId.length > 10) && // Basic format check
productId.matches(Regex("^[A-Za-z0-9]+$")) // Alphanumeric format
}

/**
* Validates the must-have-issuer-or-clinician-name invariant
* Expression: "v.is.exists() or v.cn.exists()"
*/
fun validateIssuerOrClinicanName(issuer: StringType?, clinicianName: StringType?): Boolean {
return !issuer?.value.isNullOrBlank() || !clinicianName?.value.isNullOrBlank()
}

/**
* Validates National ID Document Type against the identifier type value set
* From: http://terminology.hl7.org/CodeSystem/v2-0203 (extensible)
*/
fun validateNationalIdDocumentType(ndt: String?): Boolean {
if (ndt.isNullOrBlank()) return true // Optional field

// Common identifier types from HL7 v2-0203
val validTypes = setOf(
"DL", "PPN", "BRN", "MR", "DR", "SS", "SB", "NNCZE", "NNxxx", "MD", "DH", "BON"
)

return validTypes.contains(ndt.uppercase()) || ndt.length <= 10
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ group DemographicsToPatient(source lm:DVCLogicalModel , target patient: Patient,
lm.dob as dob -> patient.birthDate = dob "setDateofBirth";
lm.sex as sex then ExtractGender(sex, patient) "Patient Gender";
//lm.sex as sex -> patient.gender = sex "setSex";
lm.nid as id -> patient.identifier as identifier, identifier.value = id "setNationalIdentifier";
lm.nid as id -> patient.identifier as identifier then {
id -> identifier.value = id "setNationalIdentifierValue";
lm.ndt as ndt -> identifier.type as type, type.coding as coding, coding.code = ndt "setNationalIdentifierType";
} "setNationalIdentifier";
lm.guardian as guardian -> patient.contact as parentContact, parentContact.name as parentName then nameToHumanName(guardian, parentName) "setGuardianName";
}

Expand Down
Loading