2
2
3
3
module FHIRMapper
4
4
class VaccinationRecord
5
+ class UnknownVaccine < StandardError
6
+ end
7
+
5
8
delegate_missing_to :@vaccination_record
6
9
7
10
def initialize ( vaccination_record )
@@ -47,6 +50,76 @@ def fhir_record
47
50
immunisation
48
51
end
49
52
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
+
50
123
private
51
124
52
125
def fhir_identifier
@@ -77,6 +150,19 @@ def fhir_status
77
150
end
78
151
end
79
152
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
+
80
166
def fhir_site
81
167
site_info =
82
168
::VaccinationRecord ::DELIVERY_SITE_SNOMED_CODES_AND_TERMS [ delivery_site ]
@@ -92,6 +178,18 @@ def fhir_site
92
178
)
93
179
end
94
180
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
+
95
193
def fhir_route
96
194
FHIR ::CodeableConcept . new (
97
195
coding : [
@@ -104,6 +202,18 @@ def fhir_route
104
202
)
105
203
end
106
204
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
+
107
217
def fhir_dose_quantity
108
218
FHIR ::Quantity . new (
109
219
value : dose_volume_ml . to_f ,
@@ -113,18 +223,73 @@ def fhir_dose_quantity
113
223
)
114
224
end
115
225
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
+
116
261
def fhir_user_performer ( reference_id :)
117
262
FHIR ::Immunization ::Performer . new (
118
263
actor : FHIR ::Reference . new ( reference : "##{ reference_id } " )
119
264
)
120
265
end
121
266
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
+
122
281
def fhir_org_performer
123
282
FHIR ::Immunization ::Performer . new (
124
283
actor : Organisation . fhir_reference ( ods_code : performed_ods_code )
125
284
)
126
285
end
127
286
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
+
128
293
def fhir_reason_code
129
294
FHIR ::CodeableConcept . new (
130
295
coding : [
@@ -139,5 +304,31 @@ def fhir_protocol_applied
139
304
doseNumberPositiveInt : dose_sequence
140
305
)
141
306
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
142
333
end
143
334
end
0 commit comments