Skip to content

Commit

Permalink
feat(#147): adding value types (#149)
Browse files Browse the repository at this point in the history
  • Loading branch information
witash authored Nov 13, 2024
1 parent 0bca7ae commit dcb5e61
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 21 deletions.
6 changes: 2 additions & 4 deletions mediator/src/controllers/cht.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,8 @@ export async function createEncounter(chtReport: any) {
}

for (const entry of chtReport.observations) {
if (entry.valueCode || entry.valueString || entry.valueDateTime) {
const observation = buildFhirObservationFromCht(chtReport.patient_uuid, fhirEncounter, entry);
createFhirResource(observation);
}
const observation = buildFhirObservationFromCht(chtReport.patient_uuid, fhirEncounter, entry);
createFhirResource(observation);
}

return { status: 200, data: {} };
Expand Down
45 changes: 34 additions & 11 deletions mediator/src/mappers/cht.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,39 @@ export function buildFhirEncounterFromCht(chtReport: any): fhir4.Encounter {
return encounter
}

/*
* Copy the value from the form mapping to observation
* value are expected to already have been validated
* for types which are native to JSON (Integer, String, Boolean) just copy them exactly
* copy Time and DateTime as strings (they have been validated already)
* valueQuantity will be an object, but also just copy it directly
*/
function copyObservationValue(observation: any, entry: any) {
const copyValueTypes = [
'valueString',
'valueDateTime',
'valueTime',
'valueBoolean',
'valueInteger',
'valueQuantity'
];
copyValueTypes.forEach((name) => {
if (entry.hasOwnProperty(name)) {
observation[name] = entry[name];
}
});

// valueCode needs a little bit of conversion; we are not requiring cht forms to
// map to codeableConcept, just to give the string
if ('valueCode' in entry){
observation.valueCodeableConcept = {
coding: [{
code: entry['valueCode']
}]
};
}
}

export function buildFhirObservationFromCht(patient_id: string, encounter: fhir4.Encounter, entry: any): fhir4.Observation {
const patientRef: fhir4.Reference = {
reference: `Patient/${patient_id}`,
Expand Down Expand Up @@ -130,17 +163,7 @@ export function buildFhirObservationFromCht(patient_id: string, encounter: fhir4
issued: now
};

if ('valueCode' in entry){
observation.valueCodeableConcept = {
coding: [{
code: entry['valueCode']
}]
};
} else if ('valueDateTime' in entry){
observation.valueDateTime = entry['valueDateTime'];
} else if ('valueString' in entry){
observation.valueString = entry['valueString'];
}
copyObservationValue(observation, entry);

return observation;
}
Expand Down
41 changes: 41 additions & 0 deletions mediator/src/middlewares/schemas/cht.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import joi from 'joi';

export const ChtPatientSchema = joi.object({
doc: joi.object({
_id: joi.string().uuid().required(),
name: joi.string().required(),
phone: joi.string().required(),
date_of_birth: joi.string().required(),
sex: joi.string().required(),
patient_id: joi.string().required()
}).required()
});

export const ChtPatientIdsSchema = joi.object({
doc: joi.object({
patient_id: joi.string().required(),
external_id: joi.string().required()
})
});

export const ValueTypeSchema = joi.object({
code: joi.string().required(),
valueString: joi.string(),
valueCode: joi.string(),
valueBoolean: joi.boolean(),
valueInteger: joi.number().integer(),
valueDateTime: joi.string().isoDate(),
valueTime: joi.string().regex(/^([01]\d|2[0-3]):([0-5]\d)(:[0-5]\d(\.\d{1,3})?)?$/), // matches HH:mm[:ss[.SSS]]
valueQuantity: joi.object({
value: joi.number().required(),
unit: joi.string().required(),
system: joi.string().uri().optional(),
code: joi.string().optional()
})
}).or('valueString', 'valueCode', 'valueBoolean', 'valueInteger', 'valueDateTime', 'valueTime', 'valueQuantity');

export const ChtEncounterFormSchema = joi.object({
patient_uuid: joi.string().required(),
reported_date: joi.number().required(), //timestamp
observations: joi.array().items(ValueTypeSchema).optional()
});
20 changes: 14 additions & 6 deletions mediator/src/middlewares/schemas/tests/cht-request-factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,6 @@ export const ChtPregnancyForm = Factory.define('chtPregnancyDoc')
"code": "17a57368-5f59-42c8-aaab-f2774d21501e",
"valueCode": "ea6a020e-05cd-4fea-b618-abd7494ac571"
},
{
"code": "17a57368-5f59-42c8-aaab-f2774d21501e",
"valueCode": false
},
{
"code": "17a57368-5f59-42c8-aaab-f2774d21501e",
"valueCode": "0d9e45d6-9288-494e-841c-80f3f9b8e126"
Expand All @@ -75,7 +71,19 @@ export const ChtPregnancyForm = Factory.define('chtPregnancyDoc')
"valueDateTime": "2024-08-26"
},
{
"code": "13179cce-a424-43d7-9ad1-dce7861946e8",
"valueString": ""
"code": "73179cce-a424-43d7-9ad1-dce7861946e8",
"valueString": "String"
},
{
"code": "53179cce-a424-43d7-9ad1-dce7861946e8",
"valueQuantity": { "value": 160, "unit": "kg" }
},
{
"code": "37a57368-5f59-42c8-aaab-f2774d21501e",
"valueBoolean": false
},
{
"code": "47a57368-5f54-42c8-aaab-f2774d21501e",
"valueInteger": 12
}
]);
5 changes: 5 additions & 0 deletions mediator/src/routes/cht.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Router } from 'express';
import { requestHandler } from '../utils/request';
import { validateBodyAgainst } from '../middlewares';
import { ChtPatientSchema, ChtPatientIdsSchema, ChtEncounterFormSchema } from '../middlewares/schemas/cht';
import { createPatient, updatePatientIds, createEncounter } from '../controllers/cht'

const router = Router();
Expand All @@ -8,16 +10,19 @@ const resourceType = 'Patient';

router.post(
'/patient',
validateBodyAgainst(ChtPatientSchema),
requestHandler((req) => createPatient(req.body))
);

router.post(
'/patient_ids',
validateBodyAgainst(ChtPatientIdsSchema),
requestHandler((req) => updatePatientIds(req.body))
);

router.post(
'/encounter',
validateBodyAgainst(ChtEncounterFormSchema),
requestHandler((req) => createEncounter(req.body))
);

Expand Down
53 changes: 53 additions & 0 deletions mediator/src/routes/tests/cht.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import request from 'supertest';
import app from '../../..';
import { ChtPatientFactory, ChtPatientIdsFactory, ChtPregnancyForm } from '../../middlewares/schemas/tests/cht-request-factories';

describe('POST /cht/patient', () => {
it('doesn\'t accept incoming request with invalid patient resource', async () => {
const data = ChtPatientFactory.build();
delete data.doc._id;

const res = await request(app).post('/cht/patient').send(data);

expect(res.status).toBe(400);
expect(res.body.valid).toBe(false);
expect(res.body.message).toMatchInlineSnapshot(
`""doc._id" is required"`
);
});
});

describe('POST /cht/patient_ids', () => {
it('doesn\'t accept incoming request with invalid patient resource', async () => {
const data = ChtPatientIdsFactory.build();
delete data.doc.patient_id;

const res = await request(app).post('/cht/patient_ids').send(data);

expect(res.status).toBe(400);
expect(res.body.valid).toBe(false);
expect(res.body.message).toMatchInlineSnapshot(
`""doc.patient_id" is required"`
);
});
});

describe('POST /cht/encounter', () => {
it('doesn\'t accept incoming request with invalid form', async () => {
const data = ChtPregnancyForm.build();

// push an invalid observation
data.observations.push({
"code": "17a57368-5f59-42c8-aaab-f2774d21501e",
"valueDateTime": "This is not a valid date"
})

const res = await request(app).post('/cht/encounter').send(data);

expect(res.status).toBe(400);
expect(res.body.valid).toBe(false);
expect(res.body.message).toMatchInlineSnapshot(
`""observations[13].valueDateTime" must be in iso format"`
);
});
});

0 comments on commit dcb5e61

Please sign in to comment.