Skip to content

Commit c0bf129

Browse files
Merge pull request #4335 from nhsuk/vaccination-from-fhir-record
Create `VaccinationRecord` from FHIR record
2 parents b035a0a + 72d6e2b commit c0bf129

File tree

8 files changed

+1095
-4
lines changed

8 files changed

+1095
-4
lines changed

app/lib/fhir_mapper/vaccination_record.rb

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
module FHIRMapper
44
class VaccinationRecord
5+
class UnknownVaccine < StandardError
6+
end
7+
58
delegate_missing_to :@vaccination_record
69

710
def initialize(vaccination_record)
@@ -47,6 +50,76 @@ def fhir_record
4750
immunisation
4851
end
4952

53+
def self.from_fhir_record(fhir_record, patient:, team:)
54+
attrs = {}
55+
56+
# attrs[:source] = "nhs_immunisations_api"
57+
58+
attrs[:patient] = patient
59+
60+
attrs[:nhs_immunisations_api_id] = fhir_record.id
61+
attrs[:nhs_immunisations_api_synced_at] = Time.current
62+
63+
attrs[:programme] = programme_from_fhir(fhir_record)
64+
65+
attrs[:performed_at] = Time.zone.parse(fhir_record.occurrenceDateTime)
66+
attrs[:outcome] = outcome_from_fhir(fhir_record)
67+
68+
location_system = fhir_record.location.identifier.system
69+
location_value = fhir_record.location.identifier.value
70+
unless location_value == "X99999"
71+
case location_system
72+
when "https://fhir.hl7.org.uk/Id/urn-school-number"
73+
attrs[:location] = Location.find_by(urn: location_value)
74+
when "https://fhir.nhs.uk/Id/ods-organization-code"
75+
attrs[:location] = Location.find_by(ods_code: location_value)
76+
end
77+
end
78+
79+
if attrs[:location].nil?
80+
attrs[:location_name] = fhir_record.location.identifier.value
81+
end
82+
83+
# TODO: There is also a `display` field which could be used to identify the origin of the record,
84+
# but this is not marked as required on the schema, so is likely to be unreliable
85+
attrs[:performed_ods_code] = org_performer_ods_code_from_fhir(fhir_record)
86+
87+
user_performer_name = user_performer_name_from_fhir(fhir_record)
88+
attrs[:performed_by_given_name] = user_performer_name&.given&.first
89+
attrs[:performed_by_family_name] = user_performer_name&.family
90+
91+
attrs[:delivery_method] = delivery_method_from_fhir(fhir_record)
92+
attrs[:delivery_site] = site_from_fhir(fhir_record)
93+
94+
attrs[:dose_sequence] = fhir_record
95+
.protocolApplied
96+
.first
97+
.doseNumberPositiveInt
98+
99+
attrs[:vaccine] = Vaccine.from_fhir_record(fhir_record)
100+
101+
if attrs[:vaccine] && team
102+
attrs[:batch] = batch_from_fhir(
103+
fhir_record,
104+
vaccine: attrs[:vaccine],
105+
team:
106+
)
107+
attrs[:full_dose] = full_dose_from_fhir(
108+
fhir_record,
109+
vaccine: attrs[:vaccine]
110+
)
111+
else
112+
attrs[:notes] = vaccine_batch_notes_from_fhir(fhir_record)
113+
attrs[:full_dose] = true
114+
115+
Sentry.capture_exception(
116+
UnknownVaccine.new(fhir_record.vaccineCode.coding.first.code)
117+
)
118+
end
119+
120+
::VaccinationRecord.new(attrs)
121+
end
122+
50123
private
51124

52125
def fhir_identifier
@@ -77,6 +150,19 @@ def fhir_status
77150
end
78151
end
79152

153+
private_class_method def self.outcome_from_fhir(fhir_record)
154+
case fhir_record.status
155+
when "completed"
156+
"administered"
157+
when "not-done"
158+
# "refused"
159+
# TODO: handle this more gracefully
160+
raise "Cannot import not-done vaccination records"
161+
else
162+
raise "Unexpected vaccination status: #{fhir_record.status}. Expected only 'completed' or 'not-done'"
163+
end
164+
end
165+
80166
def fhir_site
81167
site_info =
82168
::VaccinationRecord::DELIVERY_SITE_SNOMED_CODES_AND_TERMS[delivery_site]
@@ -92,6 +178,18 @@ def fhir_site
92178
)
93179
end
94180

181+
private_class_method def self.site_from_fhir(fhir_record)
182+
site_code =
183+
fhir_record
184+
.site
185+
&.coding
186+
&.find { it.system == "http://snomed.info/sct" }
187+
&.code
188+
::VaccinationRecord::DELIVERY_SITE_SNOMED_CODES_AND_TERMS
189+
.find { |_key, value| value.first == site_code }
190+
&.first
191+
end
192+
95193
def fhir_route
96194
FHIR::CodeableConcept.new(
97195
coding: [
@@ -104,6 +202,18 @@ def fhir_route
104202
)
105203
end
106204

205+
private_class_method def self.delivery_method_from_fhir(fhir_record)
206+
route_code =
207+
fhir_record
208+
.route
209+
&.coding
210+
&.find { it.system == "http://snomed.info/sct" }
211+
&.code
212+
::VaccinationRecord::DELIVERY_METHOD_SNOMED_CODES_AND_TERMS
213+
.find { |_key, value| value.first == route_code }
214+
&.first
215+
end
216+
107217
def fhir_dose_quantity
108218
FHIR::Quantity.new(
109219
value: dose_volume_ml.to_f,
@@ -113,18 +223,73 @@ def fhir_dose_quantity
113223
)
114224
end
115225

226+
private_class_method def self.dose_volume_ml_from_fhir(fhir_record)
227+
dq = fhir_record.doseQuantity
228+
if dq.system == "http://unitsofmeasure.org" && dq.code == "ml"
229+
dq.value.to_f
230+
else
231+
raise "Unknown dose quantity system: #{dq.system} and code: #{dq.code}"
232+
end
233+
end
234+
235+
private_class_method def self.full_dose_from_fhir(fhir_record, vaccine:)
236+
if vaccine.programme.type == :flu && vaccine.method == :nasal
237+
fhir_record.doseQuantity.value >= vaccine.dose_volume_ml
238+
end
239+
240+
true
241+
end
242+
243+
private_class_method def self.vaccine_batch_notes_from_fhir(fhir_record)
244+
fhir_vaccine =
245+
fhir_record.vaccineCode.coding.find do
246+
it.system == "http://snomed.info/sct"
247+
end
248+
249+
vaccine_snomed_code = fhir_vaccine.code
250+
vaccine_description = fhir_vaccine.display.presence
251+
252+
batch_number = fhir_record.lotNumber
253+
batch_expiry = fhir_record.expirationDate
254+
255+
"SNOMED product code: #{vaccine_snomed_code}\n" \
256+
"#{"SNOMED description: #{vaccine_description}\n" if vaccine_description}" \
257+
"Batch number: #{batch_number}\n" \
258+
"Batch expiry: #{batch_expiry}"
259+
end
260+
116261
def fhir_user_performer(reference_id:)
117262
FHIR::Immunization::Performer.new(
118263
actor: FHIR::Reference.new(reference: "##{reference_id}")
119264
)
120265
end
121266

267+
private_class_method def self.user_performer_name_from_fhir(fhir_record)
268+
performer_references =
269+
fhir_record
270+
.performer
271+
.reject { it.actor&.type == "Organization" }
272+
.map { it.actor.reference&.sub("#", "") }
273+
user_actor =
274+
fhir_record.contained.find do |c|
275+
c.id.in?(performer_references) && c.resourceType == "Practitioner"
276+
end
277+
user_actor&.name&.find { it&.use == "official" } ||
278+
user_actor&.name&.first
279+
end
280+
122281
def fhir_org_performer
123282
FHIR::Immunization::Performer.new(
124283
actor: Organisation.fhir_reference(ods_code: performed_ods_code)
125284
)
126285
end
127286

287+
private_class_method def self.org_performer_ods_code_from_fhir(fhir_record)
288+
org_actor =
289+
fhir_record.performer.find { it.actor&.type == "Organization" }&.actor
290+
org_actor&.identifier&.value
291+
end
292+
128293
def fhir_reason_code
129294
FHIR::CodeableConcept.new(
130295
coding: [
@@ -139,5 +304,31 @@ def fhir_protocol_applied
139304
doseNumberPositiveInt: dose_sequence
140305
)
141306
end
307+
308+
private_class_method def self.programme_from_fhir(fhir_record)
309+
target_diseases = fhir_record.protocolApplied.first.targetDisease
310+
target_diseases_codes =
311+
target_diseases.map do |disease|
312+
disease
313+
.coding
314+
.find { |coding| coding.system == "http://snomed.info/sct" }
315+
.code
316+
end
317+
# This may need to change when we start consuming programmes which have multiple target diseases, eg MMR
318+
target_disease_code = target_diseases_codes.first
319+
320+
::Programme.find_by(
321+
type: ::Programme::SNOMED_TARGET_DISEASE_CODES.key(target_disease_code)
322+
)
323+
end
324+
325+
private_class_method def self.batch_from_fhir(fhir_record, vaccine:, team:)
326+
::Batch.create_with(archived_at: Time.current).find_or_create_by!(
327+
expiry: fhir_record.expirationDate&.to_date,
328+
name: fhir_record.lotNumber.to_s,
329+
team:,
330+
vaccine:
331+
)
332+
end
142333
end
143334
end

app/lib/fhir_mapper/vaccine.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ def fhir_codeable_concept
2525
)
2626
end
2727

28+
def self.from_fhir_record(fhir_record)
29+
snomed_product_code =
30+
fhir_record
31+
.vaccineCode
32+
.coding
33+
.find { it.system == "http://snomed.info/sct" }
34+
.code
35+
::Vaccine.find_by(snomed_product_code:)
36+
end
37+
2838
def fhir_manufacturer_reference
2939
FHIR::Reference.new(display: manufacturer)
3040
end

app/models/vaccination_record.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ class VaccinationRecord < ApplicationRecord
173173

174174
delegate :fhir_record, to: :fhir_mapper
175175

176+
class << self
177+
delegate :from_fhir_record, to: FHIRMapper::VaccinationRecord
178+
end
179+
176180
def academic_year = performed_at.to_date.academic_year
177181

178182
def not_administered?

config/feature_flags.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ enqueue_sync_vaccination_records_to_nhs: Send new vaccinations recorded by
1212
immunisations_fhir_api_integration: Master switch to control communications
1313
with NHS Immunisations FHIR API.
1414

15+
immunisations_fhir_api_integration_search: Master switch to control whether
16+
search requests can be made to the NHS Immunisations FHIR API.
17+
1518
import_choose_academic_year: Add an option to choose the academic year when
1619
importing patients during the preparation period.
1720

0 commit comments

Comments
 (0)