From edd6ee696cb9cfb2772758e5b147118c1f58c77d Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Sat, 31 Aug 2024 16:10:32 +0100 Subject: [PATCH 1/3] Add find_by to do patient search in PDS --- app/lib/nhs/pds/patient.rb | 23 ++++++++++++ lib/tasks/pds.rake | 31 ++++++++++++++++ spec/lib/nhs/pds/patient_spec.rb | 64 ++++++++++++++++++++++++++++++++ spec/lib/nhs/pds_spec.rb | 22 +---------- 4 files changed, 119 insertions(+), 21 deletions(-) create mode 100644 spec/lib/nhs/pds/patient_spec.rb diff --git a/app/lib/nhs/pds/patient.rb b/app/lib/nhs/pds/patient.rb index 055fb6cebe..12399cac4f 100644 --- a/app/lib/nhs/pds/patient.rb +++ b/app/lib/nhs/pds/patient.rb @@ -1,9 +1,32 @@ # frozen_string_literal: true module NHS::PDS::Patient + SEARCH_FIELDS = %w[ + _fuzzy-match + _exact-match + _history + _max-results + family + given + gender + birthdate + death-date + email + phone + address-postcode + general-practitioner + ].freeze class << self def find(nhs_number) NHS::PDS.connection.get("Patient/#{nhs_number}") end + + def find_by(**attributes) + if (missing_attrs = (attributes.keys.map(&:to_s) - SEARCH_FIELDS)).any? + raise "Unrecognised attributes: #{missing_attrs.join(", ")}" + end + + NHS::PDS.connection.get("Patient", attributes) + end end end diff --git a/lib/tasks/pds.rake b/lib/tasks/pds.rake index c5f2fc3222..c8628f2e42 100755 --- a/lib/tasks/pds.rake +++ b/lib/tasks/pds.rake @@ -18,5 +18,36 @@ namespace :pds do puts response.body end end + + desc "Find patient using patient info" + task find_by: :environment do |_, _args| + query = { + "_fuzzy-match" => ENV["_fuzzy_match"], + "_exact-match" => ENV["_exact_match"], + "_history" => ENV["_history"], + "_max-results" => ENV["_max_results"], + "given" => ENV["given"], + "family" => ENV["family"], + "gender" => ENV["gender"], + "birthdate" => ENV["birthdate"], + "death-date" => ENV["death_date"], + "email" => ENV["email"], + "phone" => ENV["phone"], + "address-postcode" => ENV["address_postcode"], + "general-practitioner" => ENV["general_practitioner"] + }.compact + response = NHS::PDS::Patient.find_by(**query) + + $stdout.puts response.status unless response.status == 200 + if $stdout.tty? + puts response.env.url + puts "" + puts response.headers.map { "#{_1}: #{_2}" } + puts "" + puts JSON.pretty_generate(JSON.parse(response.body)) + else + puts response.body + end + end end end diff --git a/spec/lib/nhs/pds/patient_spec.rb b/spec/lib/nhs/pds/patient_spec.rb new file mode 100644 index 0000000000..4ba7089299 --- /dev/null +++ b/spec/lib/nhs/pds/patient_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +describe NHS::PDS::Patient do + before do + allow(NHS::API).to receive(:connection).and_return( + Faraday.new do |builder| + stubbed_requests.each do |request, response| + builder.adapter :test do |stub| + stub.get(request) { response } + end + end + end + ) + end + + describe ".find" do + let(:stubbed_requests) do + [ + [ + "https://sandbox.api.service.nhs.uk/personal-demographics/FHIR/R4/Patient/9449306168", + [200, {}, "patient record as json"] + ] + ] + end + + it "sends a GET request to retrieve a patient by their NHS number" do + response = described_class.find("9449306168").body + + expect(response).to eq "patient record as json" + end + end + + describe ".find_by" do + let(:stubbed_requests) do + [ + [ + "https://sandbox.api.service.nhs.uk/personal-demographics/FHIR/R4/Patient", + [200, {}, "patient record as json"] + ] + ] + end + + it "sends a GET request to with the provided attributes" do + response = + described_class.find_by( + family: "Lawman", + gender: "female", + birthdate: "eq1939-01-09" + ) + + expect(response.body).to eq "patient record as json" + end + + it "raises an error if an unrecognised attribute is provided" do + expect { + described_class.find_by( + given: "Eldreda", + family_name: "Lawman", + date_of_birth: "1939-01-09" + ) + }.to raise_error("Unrecognised attributes: family_name, date_of_birth") + end + end +end diff --git a/spec/lib/nhs/pds_spec.rb b/spec/lib/nhs/pds_spec.rb index 02b7021283..fe47a0b43d 100644 --- a/spec/lib/nhs/pds_spec.rb +++ b/spec/lib/nhs/pds_spec.rb @@ -1,17 +1,7 @@ # frozen_string_literal: true describe NHS::PDS do - before do - allow(NHS::API).to receive(:connection).and_return( - Faraday.new do |builder| - builder.adapter :test do |stub| - stub.get( - "https://sandbox.api.service.nhs.uk/personal-demographics/FHIR/R4/Patient/9000000009" - ) { [200, {}, {}.to_json] } - end - end - ) - end + before { allow(NHS::API).to receive(:connection).and_return(Faraday.new) } describe ".connection" do it "sets the url" do @@ -20,14 +10,4 @@ ).to eq "https://sandbox.api.service.nhs.uk/personal-demographics/FHIR/R4" end end - - describe NHS::PDS::Patient do - describe ".find_patient" do - it "sends a GET request to retrieve a patient by their NHS number" do - response = described_class.find("9000000009").body - - expect(response).to eq "{}" - end - end - end end From 7c269a395f3ea413c7e28a355c344103a46ad7ff Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Mon, 2 Sep 2024 17:35:48 +0100 Subject: [PATCH 2/3] Update model PDS::Patient to use new pds library --- app/models/pds/patient.rb | 12 +-- spec/fixtures/patient_record.json | 125 ++++++++++++++++++++++++++++++ spec/models/pds/patient_spec.rb | 21 ++--- 3 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 spec/fixtures/patient_record.json diff --git a/app/models/pds/patient.rb b/app/models/pds/patient.rb index dabfef3ea5..b4ed48b092 100644 --- a/app/models/pds/patient.rb +++ b/app/models/pds/patient.rb @@ -7,16 +7,8 @@ class PDS::Patient class << self def find(nhs_number) - response = - JSON.parse( - Net::HTTP.get( - URI( - "https://sandbox.api.service.nhs.uk/personal-demographics/FHIR/R4/Patient/#{nhs_number}" - ), - "X-Request-ID": SecureRandom.uuid - ) - ) - from_pds_fhir_response(response) + response = NHS::PDS::Patient.find(nhs_number) + from_pds_fhir_response(JSON.parse(response.body)) end private diff --git a/spec/fixtures/patient_record.json b/spec/fixtures/patient_record.json new file mode 100644 index 0000000000..9cd2d5a8d8 --- /dev/null +++ b/spec/fixtures/patient_record.json @@ -0,0 +1,125 @@ +{ + "entry": [ + { + "fullUrl": "https://int.api.service.nhs.uk/personal-demographics/FHIR/R4/Patient/9449306168", + "resource": { + "address": [ + { + "extension": [ + { + "extension": [ + { + "url": "type", + "valueCoding": { + "code": "PAF", + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-AddressKeyType" + } + }, + { + "url": "value", + "valueString": "07352931" + } + ], + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-AddressKey" + } + ], + "id": "gon1", + "line": ["103 PRESTON LANE", "TADWORTH", "SURREY"], + "period": { + "start": "2011-06-23" + }, + "postalCode": "KT20 5HJ", + "use": "home" + } + ], + "birthDate": "1939-01-09", + "gender": "female", + "generalPractitioner": [ + { + "id": "dp7s", + "identifier": { + "period": { + "start": "1951-08-09" + }, + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "H81109" + }, + "type": "Organization" + } + ], + "id": "9449306168", + "identifier": [ + { + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSNumberVerificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "code": "01", + "display": "Number present and verified", + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-NHSNumberVerificationStatus", + "version": "1.0.0" + } + ] + } + } + ], + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9449306168" + } + ], + "meta": { + "security": [ + { + "code": "U", + "display": "unrestricted", + "system": "http://terminology.hl7.org/CodeSystem/v3-Confidentiality" + } + ], + "versionId": "6" + }, + "name": [ + { + "family": "LAWMAN", + "given": ["ELDREDA"], + "id": "frs8", + "period": { + "start": "1978-06-14" + }, + "prefix": ["MS"], + "use": "usual" + } + ], + "resourceType": "Patient", + "telecom": [ + { + "id": "6FF70BFB", + "period": { + "start": "2021-12-15" + }, + "system": "email", + "use": "home", + "value": "jagdish.khunti1@nhs.net" + }, + { + "id": "4B88618D", + "period": { + "start": "2021-12-15" + }, + "system": "phone", + "use": "mobile", + "value": "07463657671" + } + ] + }, + "search": { + "score": 1 + } + } + ], + "resourceType": "Bundle", + "timestamp": "2024-08-31T10:40:34+00:00", + "total": 1, + "type": "searchset" +} diff --git a/spec/models/pds/patient_spec.rb b/spec/models/pds/patient_spec.rb index 413250d305..79ad293063 100644 --- a/spec/models/pds/patient_spec.rb +++ b/spec/models/pds/patient_spec.rb @@ -6,25 +6,20 @@ File.read("spec/support/pds-get-patient-response.json") end let(:request_id) { "123e4567-e89b-12d3-a456-426614174000" } + let(:patient_json) do + File.read(Rails.root.join("spec/fixtures/patient_record.json")) + end before do - allow(SecureRandom).to receive(:uuid).and_return(request_id) - stub_request( - :get, - "https://sandbox.api.service.nhs.uk/personal-demographics/FHIR/R4/Patient/9000000009" - ).with(headers: { "X-Request-ID" => request_id }).to_return( - status: 200, - body: json_response + allow(NHS::PDS::Patient).to receive(:find).and_return( + instance_double(Faraday::Response, status: 200, body: json_response) ) end - it "returns a patient with the correct attributes" do - patient = described_class.find("9000000009") + it "calls find_patient on PDS library" do + described_class.find("9449306168") - expect(patient.nhs_number).to eq("9000000009") - expect(patient.given_name).to eq("Jane") - expect(patient.family_name).to eq("Smith") - expect(patient.date_of_birth).to eq("2010-10-22") + expect(NHS::PDS::Patient).to have_received(:find).with("9449306168") end end end From 7daa634da13bf6ad2d78f62f370987110d279b8d Mon Sep 17 00:00:00 2001 From: Misha Gorodnitzky Date: Tue, 3 Sep 2024 11:38:38 +0100 Subject: [PATCH 3/3] Add job to perform PDS lookup --- app/jobs/pds_lookup_job.rb | 24 ++++++++++++++++++++++++ spec/jobs/pds_lookup_job_spec.rb | 11 +++++++++++ 2 files changed, 35 insertions(+) create mode 100644 app/jobs/pds_lookup_job.rb create mode 100644 spec/jobs/pds_lookup_job_spec.rb diff --git a/app/jobs/pds_lookup_job.rb b/app/jobs/pds_lookup_job.rb new file mode 100644 index 0000000000..ccef131049 --- /dev/null +++ b/app/jobs/pds_lookup_job.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class PDSLookupJob < ApplicationJob + include GoodJob::ActiveJobExtensions::Concurrency + + queue_as :pds + + # NHS API imposes a limit of 5 requests per second + good_job_control_concurrency_with perform_limit: 5, + perform_throttle: [5, 1.second], + key: -> { queue_name } + + # Because the NHS API imposes a limit of 5 requests per second, we're almost + # certain to hit throttling and the default exponential backoff strategy + # appears to trigger more race conditions in the job performing code, meaning + # there’s more instances where more than 5 requests are attempted. + retry_on GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError, + attempts: :unlimited, + wait: ->(_) { rand(0.5..5) } + + def perform(**args) + NHS::PDS::Patient.find_by(**args) + end +end diff --git a/spec/jobs/pds_lookup_job_spec.rb b/spec/jobs/pds_lookup_job_spec.rb new file mode 100644 index 0000000000..8cd6427c26 --- /dev/null +++ b/spec/jobs/pds_lookup_job_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +describe PDSLookupJob, type: :job do + it "calls the NHS::PDS::Patient.find_by" do + allow(NHS::PDS::Patient).to receive(:find_by) + + described_class.perform_now(given: "name") + + expect(NHS::PDS::Patient).to have_received(:find_by).with(given: "name") + end +end