Skip to content

Commit 63b04ed

Browse files
authored
Merge pull request #3945 from nhsuk/update-vaccination-to-imms-api
Update vaccination records in the NHS immunisations api
2 parents 422e5c3 + 64751e3 commit 63b04ed

11 files changed

Lines changed: 525 additions & 116 deletions

.prettierignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ terraform/.terraform
3939
*.tfstate
4040
*.tfstate.backup
4141
scratchpad
42-
spec/fixtures/fhir/immunisation.json
42+
spec/fixtures/fhir/immunisation-create.json
43+
spec/fixtures/fhir/immunisation-update.json

app/jobs/sync_vaccination_record_to_nhs_job.rb

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,30 @@
33
class SyncVaccinationRecordToNHSJob < ApplicationJob
44
queue_as :immunisation_api
55

6+
retry_on Faraday::ServerError, wait: :polynomially_longer
7+
68
def perform(vaccination_record)
7-
if vaccination_record.nhs_immunisations_api_synced_at.present?
8-
Rails.logger.info(
9-
"Vaccination record already synced: #{vaccination_record.id}"
10-
)
11-
return
9+
if vaccination_record.not_administered?
10+
# TODO: This will be a delete
11+
raise "Vaccination record is not administered: #{vaccination_record.id}"
12+
end
13+
14+
if vaccination_record.discarded?
15+
# TODO: This will be a delete
16+
raise "Vaccination record is discarded: #{vaccination_record.id}"
1217
end
1318

14-
NHS::ImmunisationsAPI.record_immunisation(vaccination_record)
19+
last_synced_at = vaccination_record.nhs_immunisations_api_synced_at
20+
if last_synced_at.present?
21+
if last_synced_at > vaccination_record.updated_at
22+
Rails.logger.info(
23+
"Vaccination record already synced: #{vaccination_record.id}"
24+
)
25+
else
26+
NHS::ImmunisationsAPI.update_immunisation(vaccination_record)
27+
end
28+
else
29+
NHS::ImmunisationsAPI.record_immunisation(vaccination_record)
30+
end
1531
end
1632
end

app/lib/enqueue_sync_vaccination_record_to_nhs.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ def self.call(vaccination_record)
1111
vaccination_record
1212
.recorded_in_service
1313
.administered
14+
.kept
1415
.where(programmes: { type: PROGRAMME_TYPES })
1516
.includes(:programme)
1617
elsif vaccination_record.programme.type.in?(PROGRAMME_TYPES) &&
1718
vaccination_record.administered? &&
18-
vaccination_record.recorded_in_service?
19+
vaccination_record.recorded_in_service? && vaccination_record.kept?
1920
Array(vaccination_record)
2021
else
2122
return

app/lib/fhir_mapper/vaccination_record.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def initialize(vaccination_record)
99
end
1010

1111
def fhir_record
12-
immunisation = FHIR::Immunization.new
12+
immunisation = FHIR::Immunization.new(id: nhs_immunisations_api_id)
1313

1414
if performed_by_user.present?
1515
immunisation.contained << performed_by_user.fhir_practitioner(

app/lib/nhs/immunisations_api.rb

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ class << self
55
def record_immunisation(vaccination_record)
66
unless Flipper.enabled?(:immunisations_fhir_api_integration)
77
Rails.logger.info(
8-
"Not syncing vaccination record to immunisations API as the feature" \
9-
" flag is disabled: #{vaccination_record.id}"
8+
"Not recording vaccination record to immunisations API as the" \
9+
" feature flag is disabled: #{vaccination_record.id}"
1010
)
1111
return
1212
end
@@ -28,13 +28,71 @@ def record_immunisation(vaccination_record)
2828
nhs_immunisations_api_etag: 1
2929
)
3030
else
31-
raise "Error syncing vaccination record #{vaccination_record.id} to" \
31+
raise "Error recording vaccination record #{vaccination_record.id} to" \
3232
" Immunisations API: unexpected response status" \
3333
" #{response.status}"
3434
end
3535
rescue Faraday::ClientError => e
3636
if (diagnostics = extract_error_diagnostics(e&.response)).present?
37-
raise "Error syncing vaccination record #{vaccination_record.id} to" \
37+
raise "Error recording vaccination record #{vaccination_record.id} to" \
38+
" Immunisations API: #{diagnostics}"
39+
else
40+
raise
41+
end
42+
end
43+
44+
def update_immunisation(vaccination_record)
45+
unless Flipper.enabled?(:immunisations_fhir_api_integration)
46+
Rails.logger.info(
47+
"Not updating vaccination record to immunisations API as the" \
48+
" feature flag is disabled: #{vaccination_record.id}"
49+
)
50+
return
51+
end
52+
53+
if vaccination_record.nhs_immunisations_api_id.blank?
54+
raise "Vaccination record #{vaccination_record.id} missing NHS Immunisation ID"
55+
end
56+
57+
if vaccination_record.nhs_immunisations_api_etag.blank?
58+
raise "Vaccination record #{vaccination_record.id} missing ETag"
59+
end
60+
61+
nhs_id = vaccination_record.nhs_immunisations_api_id
62+
response =
63+
NHS::API.connection.put(
64+
"/immunisation-fhir-api/FHIR/R4/Immunization/#{nhs_id}",
65+
vaccination_record.fhir_record.to_json,
66+
{
67+
"Content-Type" => "application/fhir+json",
68+
"E-Tag" => vaccination_record.nhs_immunisations_api_etag
69+
}
70+
)
71+
72+
if response.status == 200
73+
vaccination_record.update!(
74+
nhs_immunisations_api_synced_at: Time.current,
75+
# This simplistic approach is based on the assumption that the NHS
76+
# Immunisations API will always simply increment the ETag. Alternative
77+
# approaches are to perform a GET after this POST to get the ETag, or
78+
# to update our record before we do this POST, which would mean we
79+
# don't need to store the ETag at all.
80+
#
81+
# However at this time it's our understanding that the API will always
82+
# increment the ETag, and, practically speaking, it's very unlikely
83+
# that our record would be updated by someone else. Hence this simple
84+
# approach.
85+
nhs_immunisations_api_etag:
86+
vaccination_record.nhs_immunisations_api_etag.to_i + 1
87+
)
88+
else
89+
raise "Error updating vaccination record #{vaccination_record.id} to" \
90+
" Immunisations API: unexpected response status" \
91+
" #{response.status}"
92+
end
93+
rescue Faraday::ClientError => e
94+
if (diagnostics = extract_error_diagnostics(e&.response)).present?
95+
raise "Error updating vaccination record #{vaccination_record.id} to" \
3896
" Immunisations API: #{diagnostics}"
3997
else
4098
raise
File renamed without changes.
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
{
2+
"id": "ffff1111-eeee-2222-dddd-3333eeee4444",
3+
"contained": [
4+
{
5+
"id": "Practitioner1",
6+
"name": [
7+
{
8+
"family": "Nightingale",
9+
"given": [
10+
"Florence"
11+
]
12+
}
13+
],
14+
"resourceType": "Practitioner"
15+
},
16+
{
17+
"id": "Patient1",
18+
"identifier": [
19+
{
20+
"system": "https://fhir.nhs.uk/Id/nhs-number",
21+
"value": "9449310475"
22+
}
23+
],
24+
"name": [
25+
{
26+
"family": "Taylor",
27+
"given": [
28+
"Sarah"
29+
]
30+
}
31+
],
32+
"gender": "unknown",
33+
"birthDate": "2011-09-09",
34+
"address": [
35+
{
36+
"postalCode": "EC1A 1BB"
37+
}
38+
],
39+
"resourceType": "Patient"
40+
}
41+
],
42+
"extension": [
43+
{
44+
"url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure",
45+
"valueCodeableConcept": {
46+
"coding": [
47+
{
48+
"system": "http://snomed.info/sct",
49+
"code": "761841000",
50+
"display": "Administration of vaccine product containing only Human papillomavirus antigen (procedure)"
51+
}
52+
]
53+
}
54+
}
55+
],
56+
"identifier": [
57+
{
58+
"system": "http://manage-vaccinations-in-schools.nhs.uk/vaccination_records",
59+
"value": "11112222-3333-4444-5555-666677778888"
60+
}
61+
],
62+
"status": "completed",
63+
"vaccineCode": {
64+
"coding": [
65+
{
66+
"system": "http://snomed.info/sct",
67+
"code": "10880211000001104",
68+
"display": "Gardasil vaccine suspension for injection 0.5ml pre-filled syringes (Merck Sharp & Dohme (UK) Ltd) (product)"
69+
}
70+
]
71+
},
72+
"patient": {
73+
"reference": "#Patient1"
74+
},
75+
"occurrenceDateTime": "2021-02-07T13:28:17.271+00:00",
76+
"recorded": "2021-02-07T13:28:17.271+00:00",
77+
"primarySource": true,
78+
"location": {
79+
"identifier": {
80+
"system": "https://fhir.nhs.uk/Id/ods-organization-code",
81+
"value": "X99999"
82+
}
83+
},
84+
"manufacturer": {
85+
"display": "Merck Sharp & Dohme"
86+
},
87+
"lotNumber": "X8U375AL",
88+
"expirationDate": "2023-03-20",
89+
"site": {
90+
"coding": [
91+
{
92+
"system": "http://snomed.info/sct",
93+
"code": "368208006",
94+
"display": "Left upper arm structure"
95+
}
96+
]
97+
},
98+
"route": {
99+
"coding": [
100+
{
101+
"system": "http://snomed.info/sct",
102+
"code": "78421000",
103+
"display": "Intramuscular"
104+
}
105+
]
106+
},
107+
"doseQuantity": {
108+
"value": 0.5,
109+
"unit": "ml",
110+
"system": "http://snomed.info/sct",
111+
"code": "258773002"
112+
},
113+
"performer": [
114+
{
115+
"actor": {
116+
"reference": "#Practitioner1"
117+
}
118+
},
119+
{
120+
"actor": {
121+
"type": "Organization",
122+
"identifier": {
123+
"system": "https://fhir.nhs.uk/Id/ods-organization-code",
124+
"value": "A9A5A"
125+
}
126+
}
127+
}
128+
],
129+
"reasonCode": [
130+
{
131+
"coding": [
132+
{
133+
"system": "http://snomed.info/sct",
134+
"code": "723620004"
135+
}
136+
]
137+
}
138+
],
139+
"protocolApplied": [
140+
{
141+
"targetDisease": [
142+
{
143+
"coding": [
144+
{
145+
"system": "http://snomed.info/sct",
146+
"code": "240532009",
147+
"display": "Human papillomavirus infection"
148+
}
149+
]
150+
}
151+
],
152+
"doseNumberPositiveInt": 1
153+
}
154+
],
155+
"resourceType": "Immunization"
156+
}

0 commit comments

Comments
 (0)