Skip to content

Commit

Permalink
Merge pull request #975 from varadeth/DIV-8002
Browse files Browse the repository at this point in the history
DIV-8002 | Configurable certificate fields
  • Loading branch information
egov-joy authored Mar 2, 2022
2 parents 968b9c3 + a3dc2c4 commit 3380cf9
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 89 deletions.
10 changes: 5 additions & 5 deletions backend/certificate_api/src/routes/certificate_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ function getNumberWithOrdinal(n) {
}

function appendCommaIfNotEmpty(address, suffix) {
if (address.trim().length > 0) {
if (suffix.trim().length > 0) {
if (address?.trim().length > 0) {
if (suffix?.trim().length > 0) {
return address + ", " + suffix
} else {
return address
Expand All @@ -41,14 +41,14 @@ function concatenateReadableString(a, b) {
let address = "";
address = appendCommaIfNotEmpty(address, a);
address = appendCommaIfNotEmpty(address, b);
if (address.length > 0) {
if (address?.length > 0) {
return address
}
return "NA"
}

function formatRecipientAddress(address) {
return concatenateReadableString(address.streetAddress, address.district)
return concatenateReadableString(address?.streetAddress, address?.district)
}

function formatFacilityAddress(evidence) {
Expand Down Expand Up @@ -775,7 +775,7 @@ function prepareDataForVaccineCertificateTemplate(certificateRaw, dataURL, doseT
vaccine: evidence[0].vaccine,
vaccinationDate: formatDate(evidence[0].date) + ` (Batch no. ${evidence[0].batch} )`,
vaccineValidDays: `after ${getVaccineValidDays(evidence[0].effectiveStart, evidence[0].effectiveUntil)} days`,
vaccinatedBy: evidence[0].verifier.name,
vaccinatedBy: evidence[0].verifier?.name,
vaccinatedAt: formatFacilityAddress(evidence[0]),
qrCode: dataURL,
dose: evidence[0].dose,
Expand Down
4 changes: 2 additions & 2 deletions backend/certificate_api/src/services/certificate_service.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ const convertCertificateToDCCPayload = async(certificateRaw) => {
throw new Error("EU Vaccine Details are missing from Configuration");
}
const {credentialSubject, evidence} = certificate;
const manufacturerCode = Object.keys(VACCINE_MANUF).filter(a => evidence[0].manufacturer.toLowerCase().includes(a)).length > 0 ?
Object.entries(VACCINE_MANUF).filter(([k, v]) => evidence[0].manufacturer.toLowerCase().includes(k))[0][1] : "";
const manufacturerCode = Object.keys(VACCINE_MANUF).filter(a => evidence[0].manufacturer?.toLowerCase().includes(a)).length > 0 ?
Object.entries(VACCINE_MANUF).filter(([k, v]) => evidence[0].manufacturer?.toLowerCase().includes(k))[0][1] : "";
const prophylaxisCode = Object.keys(EU_VACCINE_PROPH).filter(a => evidence[0].vaccine.toLowerCase().includes(a)).length > 0 ?
Object.entries(EU_VACCINE_PROPH).filter(([k, v]) => evidence[0].vaccine.toLowerCase().includes(k))[0][1] : "";
const vaccineCode = Object.keys(EU_VACCINE_CODE).filter(a => evidence[0].vaccine.toLowerCase().includes(a)).length > 0 ?
Expand Down
11 changes: 7 additions & 4 deletions backend/certificate_signer/config/constants.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
const ICD_MAPPINGS_KEYS = {
const CONFIG_KEYS = {
ICD: "ICD",
VACCINE_ICD: "VACCINE_ICD"
VACCINE_ICD: "VACCINE_ICD",
CERTIFICATES_OPTIONAL_FIELDS_KEY_PATH: "fieldsKeyPath",
DDCC_TEMPLATE: "DDCC_TEMPLATE",
W3C_TEMPLATE: "W3C_TEMPLATE"
};
Object.freeze(ICD_MAPPINGS_KEYS);
Object.freeze(CONFIG_KEYS);

module.exports = {
ICD_MAPPINGS_KEYS
CONFIG_KEYS
}
57 changes: 39 additions & 18 deletions backend/certificate_signer/configuration_service.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
const {Etcd3} = require('etcd3');
const config = require('./config/config');
const {ICD_MAPPINGS_KEYS} = require('./config/constants');
const {CONFIG_KEYS} = require('./config/constants');

let ICD11_MAPPINGS = null, VACCINE_ICD11_MAPPINGS = null;
let ICD11_MAPPINGS = null, VACCINE_ICD11_MAPPINGS = null, DDCC_TEMPLATE = null, W3C_TEMPLATE = null, CERTIFICATES_OPTIONAL_FIELDS_KEY_PATH = null;
let etcdClient;
let configuration;

function init() {
etcdClient = new Etcd3({hosts: config.ETCD_URL});
setUpWatcher(ICD_MAPPINGS_KEYS.ICD);
setUpWatcher(ICD_MAPPINGS_KEYS.VACCINE_ICD);
setUpWatcher(CONFIG_KEYS.ICD);
setUpWatcher(CONFIG_KEYS.VACCINE_ICD);
setUpWatcher(CONFIG_KEYS.DDCC_TEMPLATE);
setUpWatcher(CONFIG_KEYS.W3C_TEMPLATE);
setUpWatcher(CONFIG_KEYS.CERTIFICATES_OPTIONAL_FIELDS_KEY_PATH);
configuration = config.CONFIGURATION_LAYER.toLowerCase() === 'etcd' ? new etcd() : null
}

Expand All @@ -26,55 +29,73 @@ function setUpWatcher(key) {
console.log('connected');
})
.on('put', res => {
updateMappingValues(key, res.value.toString())
updateConfigValues(key, res.value.toString())
});
})
.catch(err => {
console.log(err);
});
}

function loadMappings(key) {
function loadConfigValues(key) {
let mapping;
switch(key) {
case ICD_MAPPINGS_KEYS.ICD:
case CONFIG_KEYS.ICD:
mapping = ICD11_MAPPINGS;
break;
case ICD_MAPPINGS_KEYS.VACCINE_ICD:
case CONFIG_KEYS.VACCINE_ICD:
mapping = VACCINE_ICD11_MAPPINGS;
break;
case CONFIG_KEYS.DDCC_TEMPLATE:
mapping = DDCC_TEMPLATE;
break;
case CONFIG_KEYS.CERTIFICATES_OPTIONAL_FIELDS_KEY_PATH:
mapping = CERTIFICATES_OPTIONAL_FIELDS_KEY_PATH;
break;
case CONFIG_KEYS.W3C_TEMPLATE:
mapping = W3C_TEMPLATE;
break;
}
return mapping;
}

class ConfigLayer{
async getICDMappings(key) {
let mapping = loadMappings(key)
async getConfigValue(key) {
let mapping = loadConfigValues(key)
if(mapping === null || mapping === undefined) {
if(configuration === null || configuration === null) {
return null;
}
mapping = configuration.getICDMappings(key);
updateMappingValues(key, mapping);
mapping = configuration.getConfigValue(key);
updateConfigValues(key, mapping);
}
return mapping;
}
}

const etcd = function() {
this.getICDMappings = async function(key) {
this.getConfigValue = async function(key) {
let mappingValue = (await etcdClient.get(key).string());
return mappingValue;
}
}

function updateMappingValues(key, value) {
function updateConfigValues(key, value) {
switch(key) {
case ICD_MAPPINGS_KEYS.ICD:
ICD11_MAPPINGS = value
case CONFIG_KEYS.ICD:
ICD11_MAPPINGS = value;
break;
case CONFIG_KEYS.VACCINE_ICD:
VACCINE_ICD11_MAPPINGS = value;
break;
case CONFIG_KEYS.DDCC_TEMPLATE:
DDCC_TEMPLATE = value;
break;
case CONFIG_KEYS.W3C_TEMPLATE:
W3C_TEMPLATE = value;
break;
case ICD_MAPPINGS_KEYS.VACCINE_ICD:
VACCINE_ICD11_MAPPINGS = value
case CONFIG_KEYS.CERTIFICATES_OPTIONAL_FIELDS_KEY_PATH:
CERTIFICATES_OPTIONAL_FIELDS_KEY_PATH = value;
break;
}
}
Expand Down
91 changes: 40 additions & 51 deletions backend/certificate_signer/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const signer = require('certificate-signer-library');
const Mustache = require("mustache");
const {publicKeyPem, privateKeyPem, signingKeyType} = require('./config/keys');
const identityRejectionRegex = new RegExp(IDENTITY_REJECTION_PATTERN);
const {ICD_MAPPINGS_KEYS} = require('./config/constants');
const {CONFIG_KEYS} = require('./config/constants');
console.log('Using ' + config.KAFKA_BOOTSTRAP_SERVER);
console.log('Using ' + publicKeyPem);

Expand All @@ -35,9 +35,8 @@ Mustache.escape = function (value)
const CERTIFICATE_TYPE_V2 = "certifyV2";
const CERTIFICATE_TYPE_V3 = "certifyV3";

const TEMPLATES_FOLDER = __dirname +'/config/templates/';
const DDCC_TEMPLATE_FILEPATH = TEMPLATES_FOLDER+"ddcc_w3c_certificate_payload.template";
const W3C_TEMPLATE_FILEPATH = TEMPLATES_FOLDER+"w3c_certificate_payload.template";
let DDCC_TEMPLATE;
let W3C_TEMPLATE;

const kafka = new Kafka({
clientId: 'divoc-cert',
Expand Down Expand Up @@ -82,6 +81,7 @@ let signingConfig = {

let ICD11_MAPPINGS;
let VACCINE_ICD11_MAPPINGS;
let certificateFieldsKeyPath;
let configLayerObj;
const documentLoader = {};
documentLoader[CERTIFICATE_NAMESPACE] = vaccinationContext;
Expand All @@ -102,8 +102,11 @@ documentLoader[CERTIFICATE_NAMESPACE_V2] = vaccinationContextV2;
uploadId: message.headers.uploadId ? message.headers.uploadId.toString():'',
rowId: message.headers.rowId ? message.headers.rowId.toString():'',
});
ICD11_MAPPINGS = JSON.parse(await configLayerObj.getICDMappings(ICD_MAPPINGS_KEYS.ICD));
VACCINE_ICD11_MAPPINGS = JSON.parse( await configLayerObj.getICDMappings(ICD_MAPPINGS_KEYS.VACCINE_ICD))
ICD11_MAPPINGS = JSON.parse(await configLayerObj.getConfigValue(CONFIG_KEYS.ICD));
VACCINE_ICD11_MAPPINGS = JSON.parse( await configLayerObj.getConfigValue(CONFIG_KEYS.VACCINE_ICD));
certificateFieldsKeyPath = JSON.parse(await configLayerObj.getConfigValue(CONFIG_KEYS.CERTIFICATES_OPTIONAL_FIELDS_KEY_PATH));
DDCC_TEMPLATE = await configLayerObj.getConfigValue(CONFIG_KEYS.DDCC_TEMPLATE);
W3C_TEMPLATE = await configLayerObj.getConfigValue(CONFIG_KEYS.W3C_TEMPLATE);
let jsonMessage = {};
try {
jsonMessage = JSON.parse(message.value.toString());
Expand Down Expand Up @@ -136,23 +139,22 @@ documentLoader[CERTIFICATE_NAMESPACE_V2] = vaccinationContextV2;
})
})();

function populateIdentity(cert, preEnrollmentCode) {
let identity = R.pathOr('', ['recipient', 'identity'], cert);
function populateIdentity(identity, preEnrollmentCode) {
let isURI = isURIFormat(identity);
return isURI ? identity : reinitIdentityFromPayload(identity, preEnrollmentCode);
}

function isURIFormat(param) {
let parsed;
let optionalCertificateFieldsObj;
let isURI;
try {
parsed = new URL(param);
optionalCertificateFieldsObj = new URL(param);
isURI = true;
} catch (e) {
isURI = false;
}

if (isURI && !parsed.protocol) {
if (isURI && !optionalCertificateFieldsObj.protocol) {
isURI = false;
}
return isURI;
Expand Down Expand Up @@ -187,61 +189,48 @@ function render(template, data) {
return JSON.parse(Mustache.render(template, data))
}

function transformW3(cert, certificateId) {
function transformW3(cert, certificateID) {
const certificateType = R.pathOr('', ['meta', 'certificateType'], cert);

const namespace = certificateType === CERTIFICATE_TYPE_V3 ? CERTIFICATE_NAMESPACE_V2 : CERTIFICATE_NAMESPACE;
const preEnrollmentCode = R.pathOr('', ['preEnrollmentCode'], cert);
const recipientIdentifier = populateIdentity(cert, preEnrollmentCode);
const recipientName = R.pathOr('', ['recipient', 'name'], cert);
const recipientGender = R.pathOr('', ['recipient', 'gender'], cert);
const recipientNationality = R.pathOr('', ['recipient', 'nationality'], cert);
const recipientAge = ageOfRecipient(cert.recipient); //from dob
const recipientDob = dobOfRecipient(cert.recipient);
const recipientAddressLine1 = R.pathOr('', ['recipient', 'address', 'addressLine1'], cert);
const recipientAddressLine2 = R.pathOr('', ['recipient', 'address', 'addressLine2'], cert);
const recipientAddressDistrict = R.pathOr('', ['recipient', 'address', 'district'], cert);
const recipientAddressCity = R.pathOr('', ['recipient', 'address', 'city'], cert);
const recipientAddressRegion = R.pathOr('', ['recipient', 'address', 'state'], cert);
const recipientAddressCountry = R.pathOr('', ['recipient', 'address', 'country'], cert);
const recipientAddressPostalCode = R.pathOr('', ['recipient', 'address', 'pincode'], cert);

const issuer = CERTIFICATE_ISSUER;
const issuanceDate = new Date().toISOString();

const evidenceId = CERTIFICATE_BASE_URL + certificateId;
const InfoUrl = CERTIFICATE_INFO_BASE_URL + certificateId;
const feedbackUrl = CERTIFICATE_FEEDBACK_BASE_URL + certificateId;

const batch = R.pathOr('', ['vaccination', 'batch'], cert);
const vaccine = R.pathOr('', ['vaccination', 'name'], cert);
const icd11Code = vaccine ? VACCINE_ICD11_MAPPINGS.filter(a => vaccine.toLowerCase().includes(a.vaccineName)).map(a => a.icd11Code)[0]: '';
const prophylaxis = icd11Code ? ICD11_MAPPINGS[icd11Code]["icd11Term"]: '';
const manufacturer = R.pathOr('', ['vaccination', 'manufacturer'], cert);
const batch = R.pathOr('', ['vaccination', 'batch'], cert);
const vaccinationDate = R.pathOr('', ['vaccination', 'date'], cert);
const effectiveStart = R.pathOr('', ['vaccination', 'effectiveStart'], cert);
const effectiveUntil = R.pathOr('', ['vaccination', 'effectiveUntil'], cert);
const dose = R.pathOr('', ['vaccination', 'dose'], cert);
const totalDoses = R.pathOr('', ['vaccination', 'totalDoses'], cert);
const verifierName = R.pathOr('', ['vaccinator', 'name'], cert);
const facilityName = R.pathOr('', ['facility', 'name'], cert);
const facilityAddressLine1 = R.pathOr('', ['facility', 'address', 'addressLine1'], cert);
const facilityAddressLine2 = R.pathOr('', ['facility', 'address', 'addressLine2'], cert);
const facilityAddressDistrict = R.pathOr('', ['facility', 'address', 'district'], cert);
const facilityAddressCity = R.pathOr('', ['facility', 'address', 'city'], cert);
const facilityAddressRegion = R.pathOr('', ['facility', 'address', 'state'], cert);
const facilityAddressCountry = R.pathOr(config.FACILITY_COUNTRY_CODE, ['facility', 'address', 'country'], cert);
const facilityAddressPostalCode = R.pathOr('', ['facility', 'address', 'pincode'], cert);
const issuer = CERTIFICATE_ISSUER;
const certificateId = certificateID;

const evidenceId = CERTIFICATE_BASE_URL + certificateId;
const InfoUrl = CERTIFICATE_INFO_BASE_URL + certificateId;
const feedbackUrl = CERTIFICATE_FEEDBACK_BASE_URL + certificateId;

const issuanceDate = new Date().toISOString();
let optionalCertificateFields = {};
for(const [fieldName, certificatePath] of Object.entries(certificateFieldsKeyPath)) {
let fieldValue = R.pathOr('', certificatePath, cert);
if(fieldName === "recipientIdentifier") {
fieldValue = populateIdentity(fieldValue, preEnrollmentCode);
}
else if(fieldName === "recipientAge") {
fieldValue = ageOfRecipient(cert.recipient);
}
optionalCertificateFields[fieldName] = fieldValue;
}

let data = {
namespace, recipientIdentifier, preEnrollmentCode, recipientName, recipientGender, recipientDob, recipientAge, recipientNationality,
recipientAddressLine1, recipientAddressLine2, recipientAddressDistrict, recipientAddressCity, recipientAddressRegion, recipientAddressCountry, recipientAddressPostalCode,
namespace, preEnrollmentCode,
recipientName, recipientDob,
issuer, issuanceDate, evidenceId, InfoUrl, feedbackUrl,
certificateId, batch, vaccine, icd11Code, prophylaxis, manufacturer, vaccinationDate, effectiveStart, effectiveUntil, dose, totalDoses,
verifierName,
facilityName, facilityAddressLine1, facilityAddressLine2, facilityAddressDistrict, facilityAddressCity, facilityAddressRegion, facilityAddressCountry, facilityAddressPostalCode
certificateId, batch, vaccine, icd11Code, prophylaxis, vaccinationDate, dose, totalDoses,
facilityAddressCountry,
...optionalCertificateFields
};

const template = certificateType === CERTIFICATE_TYPE_V3 ? fs.readFileSync(DDCC_TEMPLATE_FILEPATH, 'utf8') : fs.readFileSync(W3C_TEMPLATE_FILEPATH, 'utf8');
const template = certificateType === CERTIFICATE_TYPE_V3 ? DDCC_TEMPLATE : W3C_TEMPLATE;
return render(template, data);
}
18 changes: 9 additions & 9 deletions backend/certificate_signer/test/configuration_service.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
describe('should retrieve all mappings if correct configuration layer passed', () => {
const etcd3 = require('etcd3');
const config = require('../config/config');
const { ICD_MAPPINGS_KEYS } = require('../config/constants');
const { CONFIG_KEYS } = require('../config/constants');
var mockConfig = {
CONFIGURATION_LAYER: 'etcd',
ETCD_URL: 'etcd:2379'
Expand Down Expand Up @@ -53,24 +53,24 @@ describe('should retrieve all mappings if correct configuration layer passed', (
init();
});

test('should initialise two watchers each for ICD and VACCINE_ICD mapping keys', () => {
test('should initialise five times watchers each for ICD, VACCINE_ICD, DDCC_TEMPLATE, W3C_TEMPLATE and FIELDS_KEY_PATH ', () => {
expect(etcd3.Etcd3).toHaveBeenCalled();
expect(mockEtcd3Constructor.watch).toHaveBeenCalledTimes(2);
expect(mockEtcd3Constructor.watch).toHaveBeenCalledTimes(5);
});

test('should fetch values of ICD Mapping and VACCINE_ICD mapping from etcd', async() => {
const ICD = await (new ConfigLayer()).getICDMappings(ICD_MAPPINGS_KEYS.ICD);
const VACCINE_ICD = await (new ConfigLayer()).getICDMappings(ICD_MAPPINGS_KEYS.VACCINE_ICD);
const ICD = await (new ConfigLayer()).getConfigValue(CONFIG_KEYS.ICD);
const VACCINE_ICD = await (new ConfigLayer()).getConfigValue(CONFIG_KEYS.VACCINE_ICD);
expect(ICD).toEqual({})
expect(VACCINE_ICD).toEqual([])
expect(mockEtcd3Constructor.get).toHaveBeenCalledTimes(2);
expect(mockEtcd3Constructor.get).toHaveBeenCalledWith(ICD_MAPPINGS_KEYS.ICD);
expect(mockEtcd3Constructor.get).toHaveBeenCalledWith(ICD_MAPPINGS_KEYS.VACCINE_ICD);
expect(mockEtcd3Constructor.get).toHaveBeenCalledWith(CONFIG_KEYS.ICD);
expect(mockEtcd3Constructor.get).toHaveBeenCalledWith(CONFIG_KEYS.VACCINE_ICD);
});
});

describe('wrong environment variable for configuration layer', () => {
const { ICD_MAPPINGS_KEYS } = require('../config/constants');
const { CONFIG_KEYS } = require('../config/constants');
const OLD_ENV = process.env;
jest.resetModules();
var mockConfig = {
Expand All @@ -89,7 +89,7 @@ describe('wrong environment variable for configuration layer', () => {
});

test('should return null if wrong configuration passed', async() => {
const mapping = await (new services.ConfigLayer()).getICDMappings(ICD_MAPPINGS_KEYS.ICD);
const mapping = await (new services.ConfigLayer()).getConfigValue(CONFIG_KEYS.ICD);
expect(mapping).toEqual(null);
})
});
Loading

0 comments on commit 3380cf9

Please sign in to comment.