Skip to content

Commit 08fcf4c

Browse files
Add function to make search requests
`NHS::ImmunisationsAPI::search_immunisations` sends the search request to the API, and handles the response. NB: There is a bug in the API code which leads to the `Bundle.link` being incorrect. I have implemented a solution which will accept both the current incorrect version, as well as the fixed version after they deploy their fix. After that, the extra logic in `check_bundle_link_params` can be removed.
1 parent 6222d16 commit 08fcf4c

8 files changed

+891
-17
lines changed

app/components/app_vaccination_record_api_sync_status_component.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,7 @@ def additional_information_text
5353
case sync_status
5454
when :not_synced
5555
is_not_a_synced_programme =
56-
!vaccination_record.programme.type.in?(
57-
NHS::ImmunisationsAPI::PROGRAMME_TYPES
58-
)
56+
!vaccination_record.programme.can_write_to_immunisations_api?
5957
if is_not_a_synced_programme
6058
"Records are currently not synced for this programme"
6159
elsif notify_parents == false

app/lib/nhs/immunisations_api.rb

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# frozen_string_literal: true
22

33
module NHS::ImmunisationsAPI
4-
PROGRAMME_TYPES = %w[flu hpv].freeze
4+
class BundleLinkParamsMismatch < StandardError
5+
end
6+
7+
class OperationOutcomeInBundle < StandardError
8+
end
59

610
class << self
711
def sync_immunisation(vaccination_record)
@@ -187,12 +191,73 @@ def should_be_in_immunisations_api?(
187191
)
188192
vaccination_record.kept? && vaccination_record.recorded_in_service? &&
189193
vaccination_record.administered? &&
190-
vaccination_record.programme.type.in?(PROGRAMME_TYPES) &&
194+
vaccination_record.programme.can_write_to_immunisations_api? &&
191195
(ignore_nhs_number || vaccination_record.patient.nhs_number.present?) &&
192196
vaccination_record.notify_parents &&
193197
vaccination_record.patient.not_invalidated?
194198
end
195199

200+
def search_immunisations(patient, programmes:, date_from: nil, date_to: nil)
201+
unless Flipper.enabled?(:immunisations_fhir_api_integration) &&
202+
Flipper.enabled?(:immunisations_fhir_api_integration_search)
203+
Rails.logger.info(
204+
"Not searching for vaccination records in the immunisations API as one of the" \
205+
" feature flags is disabled: Patient #{patient.id}"
206+
)
207+
return
208+
end
209+
210+
if programmes.empty?
211+
raise "Cannot search for vaccination records in the immunisations API; no programmes provided."
212+
elsif !programmes.all?(&:can_read_from_immunisations_api?)
213+
raise "Cannot search for vaccination records in the immunisations API; one or more programmes is not supported."
214+
end
215+
216+
Rails.logger.info(
217+
"Searching for vaccination records in immunisations API for patient: #{patient.id}"
218+
)
219+
220+
params = {
221+
"patient.identifier" =>
222+
"https://fhir.nhs.uk/Id/nhs-number|#{patient.nhs_number}",
223+
"-immunization.target" =>
224+
programmes.map(&:snomed_target_disease_name).join(","),
225+
"-date.from" => date_from&.strftime("%F"),
226+
"-date.to" => date_to&.strftime("%F")
227+
}.compact
228+
229+
response =
230+
NHS::API.connection.get(
231+
"/immunisation-fhir-api/FHIR/R4/Immunization",
232+
params,
233+
"Content-Type" => "application/fhir+json"
234+
)
235+
236+
if response.status == 200
237+
# # To create fixtures for testing
238+
# File.write("tmp/search_response.json", response.body.to_json)
239+
# Rails.logger.debug "Successfully saved"
240+
241+
bundle = FHIR.from_contents(response.body.to_json)
242+
243+
check_bundle_link_params(bundle, params)
244+
check_operation_outcome_entry(bundle)
245+
246+
bundle
247+
else
248+
raise "Error searching for vaccination records for patient #{patient.id} in" \
249+
" Immunisations API: unexpected response status" \
250+
" #{response.status}"
251+
end
252+
rescue Faraday::ClientError => e
253+
if (diagnostics = extract_error_diagnostics(e&.response)).present?
254+
raise "Error searching for vaccination records for patient #{patient.id} in" \
255+
" Immunisations API: #{diagnostics}"
256+
else
257+
raise
258+
end
259+
end
260+
196261
private
197262

198263
def next_sync_action(vaccination_record)
@@ -264,5 +329,37 @@ def check_vaccination_record_for_create_or_update(vaccination_record)
264329
raise "Patient nhs number is missing: #{vaccination_record.id}"
265330
end
266331
end
332+
333+
def check_bundle_link_params(bundle, request_params)
334+
link = bundle.link&.find { it.relation == "self" }&.url
335+
336+
uri = URI(link)
337+
bundle_params = URI.decode_www_form(uri.query).to_h
338+
339+
# TODO: There is currently a bug in the API where the `Bundle.link` value for `-immunization.target` is
340+
# incorrectly returned as `immunization.target` rather than `-immunization.target`. Matt Jarvis has
341+
# told me that this should be fixed in their next release (3) or possibly release 4, at which point we
342+
# can remove this logic.
343+
tweaked_bundle_params =
344+
bundle_params.transform_keys do |key|
345+
key == "immunization.target" ? "-immunization.target" : key
346+
end
347+
348+
unless tweaked_bundle_params == request_params ||
349+
bundle_params == request_params
350+
raise NHS::ImmunisationsAPI::BundleLinkParamsMismatch,
351+
"Bundle link parameters do not match request parameters: #{tweaked_bundle_params} != #{request_params}"
352+
end
353+
end
354+
355+
def check_operation_outcome_entry(bundle)
356+
operation_outcome_entry =
357+
bundle.entry&.find { it.resource.resourceType == "OperationOutcome" }
358+
359+
if operation_outcome_entry.present?
360+
raise NHS::ImmunisationsAPI::OperationOutcomeInBundle,
361+
"OperationOutcome entry found in bundle: #{operation_outcome_entry.resource}"
362+
end
363+
end
267364
end
268365
end

app/models/programme.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,16 @@ def snomed_target_disease_term
141141
SNOMED_TARGET_DISEASE_TERMS.fetch(type)
142142
end
143143

144+
SNOMED_TARGET_DISEASE_NAMES = { "flu" => "FLU" }.freeze
145+
146+
def snomed_target_disease_name
147+
SNOMED_TARGET_DISEASE_NAMES.fetch(type)
148+
end
149+
150+
def can_write_to_immunisations_api? = flu? || hpv?
151+
152+
def can_read_from_immunisations_api? = flu?
153+
144154
private
145155

146156
def fhir_mapper = @fhir_mapper ||= FHIRMapper::Programme.new(self)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"resourceType": "Bundle",
3+
"type": "searchset",
4+
"link": [
5+
{
6+
"relation": "self",
7+
"url": "https://int.api.service.nhs.uk/immunisation-fhir-api/Immunization?-date.from=2025-08-01\u0026-date.to=2025-10-01\u0026immunization.target=FLU\u0026patient.identifier=https%3A%2F%2Ffhir.nhs.uk%2FId%2Fnhs-number%7C9449308357"
8+
}
9+
],
10+
"entry": [],
11+
"total": 0
12+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
{
2+
"resourceType": "Bundle",
3+
"type": "searchset",
4+
"link": [
5+
{
6+
"relation": "self",
7+
"url": "https://int.api.service.nhs.uk/immunisation-fhir-api/Immunization?-date.from=2025-08-01\u0026-date.to=2025-10-01\u0026immunization.target=FLU\u0026patient.identifier=https%3A%2F%2Ffhir.nhs.uk%2FId%2Fnhs-number%7C9449308357"
8+
}
9+
],
10+
"entry": [
11+
{
12+
"fullUrl": "https://api.service.nhs.uk/immunisation-fhir-api/Immunization/4e7f3c91-a14d-4139-bbcf-859e998d2028",
13+
"resource": {
14+
"resourceType": "Immunization",
15+
"id": "4e7f3c91-a14d-4139-bbcf-859e998d2028",
16+
"extension": [
17+
{
18+
"url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure",
19+
"valueCodeableConcept": {
20+
"coding": [
21+
{
22+
"system": "http://snomed.info/sct",
23+
"code": "884861000000100",
24+
"display": "Seasonal influenza vaccination (procedure)"
25+
}
26+
]
27+
}
28+
}
29+
],
30+
"identifier": [
31+
{
32+
"use": "official",
33+
"system": "http://manage-vaccinations-in-schools.nhs.uk/vaccination_records",
34+
"value": "71f538d8-1171-4204-aee4-17ff0b0ba0b0"
35+
}
36+
],
37+
"status": "completed",
38+
"vaccineCode": {
39+
"coding": [
40+
{
41+
"system": "http://snomed.info/sct",
42+
"code": "43208811000001106",
43+
"display": "Fluenz (trivalent) vaccine nasal suspension 0.2ml unit dose (AstraZeneca UK Ltd) (product)"
44+
}
45+
]
46+
},
47+
"patient": {
48+
"reference": "urn:uuid:e06dbb8d-ef9b-454f-9c5f-fde8460a0b6a",
49+
"type": "Patient",
50+
"identifier": {
51+
"system": "https://fhir.nhs.uk/Id/nhs-number",
52+
"value": "9449308357"
53+
}
54+
},
55+
"occurrenceDateTime": "2025-08-22T14:16:03+01:00",
56+
"recorded": "2025-08-22T14:16:05.246000+01:00",
57+
"primarySource": true,
58+
"location": {
59+
"identifier": {
60+
"system": "https://fhir.hl7.org.uk/Id/urn-school-number",
61+
"value": "100001"
62+
}
63+
},
64+
"manufacturer": {
65+
"display": "AstraZeneca"
66+
},
67+
"lotNumber": "BU5086",
68+
"expirationDate": "2025-09-30",
69+
"site": {
70+
"coding": [
71+
{
72+
"system": "http://snomed.info/sct",
73+
"code": "279549004",
74+
"display": "Nasal cavity structure"
75+
}
76+
]
77+
},
78+
"route": {
79+
"coding": [
80+
{
81+
"system": "http://snomed.info/sct",
82+
"code": "46713006",
83+
"display": "Nasal"
84+
}
85+
]
86+
},
87+
"doseQuantity": {
88+
"value": 0.2,
89+
"unit": "ml",
90+
"system": "http://snomed.info/sct",
91+
"code": "258773002"
92+
},
93+
"performer": [
94+
{
95+
"actor": {
96+
"type": "Organization",
97+
"identifier": {
98+
"system": "https://fhir.nhs.uk/Id/ods-organization-code",
99+
"value": "R1L"
100+
}
101+
}
102+
}
103+
],
104+
"reasonCode": [
105+
{
106+
"coding": [
107+
{
108+
"system": "http://snomed.info/sct",
109+
"code": "723620004"
110+
}
111+
]
112+
}
113+
],
114+
"protocolApplied": [
115+
{
116+
"targetDisease": [
117+
{
118+
"coding": [
119+
{
120+
"system": "http://snomed.info/sct",
121+
"code": "6142004",
122+
"display": "Influenza"
123+
}
124+
]
125+
}
126+
],
127+
"doseNumberPositiveInt": 1
128+
}
129+
]
130+
},
131+
"search": {
132+
"mode": "match"
133+
}
134+
},
135+
{
136+
"fullUrl": "urn:uuid:e06dbb8d-ef9b-454f-9c5f-fde8460a0b6a",
137+
"resource": {
138+
"resourceType": "Patient",
139+
"id": "9449308357",
140+
"identifier": [
141+
{
142+
"system": "https://fhir.nhs.uk/Id/nhs-number",
143+
"value": "9449308357"
144+
}
145+
]
146+
},
147+
"search": {
148+
"mode": "include"
149+
}
150+
}
151+
],
152+
"total": 1
153+
}

0 commit comments

Comments
 (0)