Skip to content

Commit 11b1987

Browse files
authored
Merge pull request #3841 from nhsuk/fhir-create-job
Add job to sync vaccination records upstream to NHS
2 parents cadfd6c + fc991b2 commit 11b1987

15 files changed

Lines changed: 553 additions & 215 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
class SyncVaccinationRecordToNHSJob < ApplicationJob
4+
queue_as :immunisation_api
5+
6+
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
12+
end
13+
14+
NHS::ImmunisationsAPI.record_immunisation(vaccination_record)
15+
end
16+
end

app/lib/mavis_cli.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ def self.progress_bar(total)
2424
require_relative "mavis_cli/gias/check_import"
2525
require_relative "mavis_cli/gias/download"
2626
require_relative "mavis_cli/gias/import"
27+
require_relative "mavis_cli/vaccination_records/sync"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
module MavisCLI
4+
module VaccinationRecords
5+
class Sync < Dry::CLI::Command
6+
desc "Sync a vaccination record to NHS Immunisations API"
7+
argument :vaccination_record_id,
8+
required: true,
9+
desc: "ID of vaccination record to sync"
10+
11+
def call(vaccination_record_id:, **)
12+
MavisCLI.load_rails
13+
14+
vaccination_record =
15+
::VaccinationRecord.find_by(id: vaccination_record_id)
16+
17+
if vaccination_record.nil?
18+
puts "Vaccination record with ID #{vaccination_record_id} not found"
19+
return
20+
end
21+
22+
if vaccination_record.nhs_immunisations_api_synced_at.present?
23+
puts "Vaccination record #{vaccination_record_id} has already been" \
24+
" synced at #{vaccination_record.nhs_immunisations_api_synced_at}"
25+
return
26+
end
27+
28+
SyncVaccinationRecordToNHSJob.perform_now(vaccination_record)
29+
puts "Successfully synced vaccination record #{vaccination_record_id}"
30+
end
31+
end
32+
end
33+
34+
register "vaccination-records" do |prefix|
35+
prefix.register "sync", VaccinationRecords::Sync
36+
end
37+
end

app/lib/nhs/immunisations_api.rb

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,67 @@
11
# frozen_string_literal: true
22

33
module NHS::ImmunisationsAPI
4-
class PatientNotFound < StandardError
5-
end
6-
74
class << self
85
def record_immunisation(vaccination_record)
9-
NHS::API.connection.post(
10-
"/immunisation-fhir-api/FHIR/R4/Immunization",
11-
vaccination_record.fhir_record.to_json,
12-
"Content-Type" => "application/fhir+json"
13-
)
14-
rescue Faraday::Error => e
15-
info = extract_error_info(e.response[:body])
16-
Rails.logger.error(
17-
"Error recording vaccination record (#{vaccination_record.id}):" \
18-
" [#{info[:code]}] #{info[:diagnostics]}"
19-
)
20-
raise e
6+
unless Flipper.enabled?(:immunisations_fhir_api_integration)
7+
Rails.logger.info(
8+
"Not syncing vaccination record to immunisations API as the feature" \
9+
" flag is disabled: #{vaccination_record.id}"
10+
)
11+
return
12+
end
13+
14+
response =
15+
NHS::API.connection.post(
16+
"/immunisation-fhir-api/FHIR/R4/Immunization",
17+
vaccination_record.fhir_record.to_json,
18+
"Content-Type" => "application/fhir+json"
19+
)
20+
21+
if response.status == 201
22+
vaccination_record.update!(
23+
nhs_immunisations_api_id:
24+
extract_nhs_id(response.headers.fetch("location")),
25+
nhs_immunisations_api_synced_at: Time.current,
26+
# We would normally retrieve this from the API response, but the NHS
27+
# Immunisations API does not return this to us, yet.
28+
nhs_immunisations_api_etag: 1
29+
)
30+
else
31+
raise "Error syncing vaccination record #{vaccination_record.id} to" \
32+
" Immunisations API: unexpected response status" \
33+
" #{response.status}"
34+
end
35+
rescue Faraday::ClientError => e
36+
if (diagnostics = extract_error_diagnostics(e&.response)).present?
37+
raise "Error syncing vaccination record #{vaccination_record.id} to" \
38+
" Immunisations API: #{diagnostics}"
39+
else
40+
raise
41+
end
2142
end
2243

23-
def extract_error_info(response_body)
24-
return { code: nil, diagnostics: "No response body" } unless response_body
44+
private
2545

26-
response = JSON.parse(response_body, symbolize_names: true)
46+
def extract_error_diagnostics(response)
47+
return nil if response.nil? || response[:body].blank?
2748

28-
if response.empty?
29-
{ code: nil, diagnostics: "No response body" }
30-
elsif response[:issue].blank?
31-
{ code: nil, diagnostics: "No issues in response" }
32-
elsif response[:issue].first[:severity] != "error"
33-
{ code: nil, diagnostics: "Issue is not an error" }
34-
else
35-
diagnostics = response[:issue].first[:diagnostics]
36-
if diagnostics.match?(/NHS Number: \d{10} is invalid.*/)
37-
diagnostics.replace("NHS Number is invalid or it doesn't exist")
38-
end
49+
begin
50+
JSON.parse(response[:body], symbolize_names: true).dig(
51+
:issue,
52+
0,
53+
:diagnostics
54+
)
55+
rescue JSON::ParserError
56+
nil
57+
end
58+
end
3959

40-
{ code: response[:issue].first[:code], diagnostics: diagnostics }
60+
def extract_nhs_id(location)
61+
if (match = location.match(%r{Immunization/([a-f0-9-]+)}))
62+
match[1]
63+
else
64+
raise UnrecognisedLocation, location
4165
end
4266
end
4367
end

app/models/vaccination_record.rb

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,45 @@
44
#
55
# Table name: vaccination_records
66
#
7-
# id :bigint not null, primary key
8-
# confirmation_sent_at :datetime
9-
# delivery_method :integer
10-
# delivery_site :integer
11-
# discarded_at :datetime
12-
# dose_sequence :integer
13-
# full_dose :boolean
14-
# location_name :string
15-
# notes :text
16-
# outcome :integer not null
17-
# pending_changes :jsonb not null
18-
# performed_at :datetime not null
19-
# performed_by_family_name :string
20-
# performed_by_given_name :string
21-
# performed_ods_code :string
22-
# uuid :uuid not null
23-
# created_at :datetime not null
24-
# updated_at :datetime not null
25-
# batch_id :bigint
26-
# patient_id :bigint
27-
# performed_by_user_id :bigint
28-
# programme_id :bigint not null
29-
# session_id :bigint
30-
# vaccine_id :bigint
7+
# id :bigint not null, primary key
8+
# confirmation_sent_at :datetime
9+
# delivery_method :integer
10+
# delivery_site :integer
11+
# discarded_at :datetime
12+
# dose_sequence :integer
13+
# full_dose :boolean
14+
# location_name :string
15+
# nhs_immunisations_api_etag :string
16+
# nhs_immunisations_api_synced_at :datetime
17+
# notes :text
18+
# outcome :integer not null
19+
# pending_changes :jsonb not null
20+
# performed_at :datetime not null
21+
# performed_by_family_name :string
22+
# performed_by_given_name :string
23+
# performed_ods_code :string
24+
# uuid :uuid not null
25+
# created_at :datetime not null
26+
# updated_at :datetime not null
27+
# batch_id :bigint
28+
# nhs_immunisations_api_id :string
29+
# patient_id :bigint
30+
# performed_by_user_id :bigint
31+
# programme_id :bigint not null
32+
# session_id :bigint
33+
# vaccine_id :bigint
3134
#
3235
# Indexes
3336
#
34-
# index_vaccination_records_on_batch_id (batch_id)
35-
# index_vaccination_records_on_discarded_at (discarded_at)
36-
# index_vaccination_records_on_patient_id (patient_id)
37-
# index_vaccination_records_on_performed_by_user_id (performed_by_user_id)
38-
# index_vaccination_records_on_programme_id (programme_id)
39-
# index_vaccination_records_on_session_id (session_id)
40-
# index_vaccination_records_on_uuid (uuid) UNIQUE
41-
# index_vaccination_records_on_vaccine_id (vaccine_id)
37+
# index_vaccination_records_on_batch_id (batch_id)
38+
# index_vaccination_records_on_discarded_at (discarded_at)
39+
# index_vaccination_records_on_nhs_immunisations_api_id (nhs_immunisations_api_id) UNIQUE
40+
# index_vaccination_records_on_patient_id (patient_id)
41+
# index_vaccination_records_on_performed_by_user_id (performed_by_user_id)
42+
# index_vaccination_records_on_programme_id (programme_id)
43+
# index_vaccination_records_on_session_id (session_id)
44+
# index_vaccination_records_on_uuid (uuid) UNIQUE
45+
# index_vaccination_records_on_vaccine_id (vaccine_id)
4246
#
4347
# Foreign Keys
4448
#

config/feature_flags.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ dev_tools: Developer tools useful for testing and debugging.
88
mesh_jobs: Export vaccination records to MESH automatically.
99

1010
offline_working: Prototype support for using Mavis offline.
11+
12+
immunisations_fhir_api_integration: Master switch to control communications with
13+
NHS Immunistaions FHIR API.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
class AddNHSImmunisationsAPISyncedAtToVaccinationRecord < ActiveRecord::Migration[
4+
8.0
5+
]
6+
def change
7+
add_column :vaccination_records,
8+
:nhs_immunisations_api_synced_at,
9+
:datetime,
10+
null: true
11+
end
12+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
class AddNHSImmunisationsAPIEtagToVaccinationRecord < ActiveRecord::Migration[
4+
8.0
5+
]
6+
def change
7+
add_column :vaccination_records,
8+
:nhs_immunisations_api_etag,
9+
:string,
10+
null: true
11+
end
12+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
class AddNHSImmunisationsAPIIdToVaccinationRecord < ActiveRecord::Migration[8.0]
4+
def change
5+
add_column :vaccination_records,
6+
:nhs_immunisations_api_id,
7+
:string,
8+
null: true
9+
add_index :vaccination_records, :nhs_immunisations_api_id, unique: true
10+
end
11+
end

db/schema.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[8.0].define(version: 2025_07_02_142922) do
13+
ActiveRecord::Schema[8.0].define(version: 2025_07_02_162254) do
1414
# These are extensions that must be enabled in order to support this database
1515
enable_extension "pg_catalog.plpgsql"
1616
enable_extension "pg_trgm"
@@ -820,8 +820,12 @@
820820
t.string "performed_ods_code"
821821
t.bigint "vaccine_id"
822822
t.boolean "full_dose"
823+
t.datetime "nhs_immunisations_api_synced_at"
824+
t.string "nhs_immunisations_api_id"
825+
t.string "nhs_immunisations_api_etag"
823826
t.index ["batch_id"], name: "index_vaccination_records_on_batch_id"
824827
t.index ["discarded_at"], name: "index_vaccination_records_on_discarded_at"
828+
t.index ["nhs_immunisations_api_id"], name: "index_vaccination_records_on_nhs_immunisations_api_id", unique: true
825829
t.index ["patient_id"], name: "index_vaccination_records_on_patient_id"
826830
t.index ["performed_by_user_id"], name: "index_vaccination_records_on_performed_by_user_id"
827831
t.index ["programme_id"], name: "index_vaccination_records_on_programme_id"

0 commit comments

Comments
 (0)