Skip to content

Commit 32f6e5a

Browse files
Merge pull request #6096 from NHSDigital/alistair/vaccs-import-deduplicate-rows
Deduplicate rows in `ImmunisationImport`s
2 parents ab61229 + 4a95a46 commit 32f6e5a

8 files changed

Lines changed: 189 additions & 89 deletions

File tree

app/models/immunisation_import.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,24 @@ def records_count
6767
private
6868

6969
def check_rows_are_unique
70-
# there is no uniqueness check for immunisations
70+
rows
71+
.map(&:full_row_deduplication_attributes)
72+
.tally
73+
.each do |full_row_deduplication_attributes, count|
74+
next if count <= 1
75+
76+
matching_rows =
77+
rows.select do
78+
it.full_row_deduplication_attributes ==
79+
full_row_deduplication_attributes
80+
end
81+
matching_rows.each do |row|
82+
row.errors.add(
83+
:base,
84+
"This record appears more than once in the file. Remove any duplicates."
85+
)
86+
end
87+
end
7188
end
7289

7390
def parse_row(data)

app/models/immunisation_import_row.rb

Lines changed: 103 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ def initialize(data:, team:, type:)
103103
@type = type&.to_sym
104104
end
105105

106-
# Convenience predicate helpers mirroring the enum on ImmunisationImport
107106
def point_of_care? = type == :point_of_care
108107

109108
def national_reporting_flu? = national_reporting? && programme&.flu?
@@ -119,80 +118,13 @@ def national_reporting_not_administered?
119118
def to_vaccination_record
120119
return if invalid? || national_reporting_not_administered? || patient.nil?
121120

122-
outcome = (administered ? "administered" : reason_not_administered_value)
123-
source =
124-
if imms_api_record?
125-
"nhs_immunisations_api"
126-
elsif offline_recording?
127-
"service"
128-
elsif point_of_care?
129-
"historical_upload"
130-
else
131-
"national_reporting"
132-
end
133-
134-
attributes = {
135-
disease_types:,
136-
dose_sequence: dose_sequence_value,
137-
full_dose: true,
138-
outcome:,
139-
patient:,
140-
performed_at_date:,
141-
performed_by_user:,
142-
performed_ods_code: performed_ods_code&.to_s,
143-
programme_type: programme&.type,
144-
protocol:,
145-
session:,
146-
supplied_by:
147-
}
148-
149-
attributes.merge!(location:, location_name:) unless imms_api_record?
150-
attributes.merge!(notify_parents: true) if session
151-
152-
if performed_by_user.nil? &&
153-
(performed_by_family_name.present? || performed_by_given_name.present?)
154-
attributes.merge!(
155-
performed_by_family_name: performed_by_family_name&.to_s,
156-
performed_by_given_name: performed_by_given_name&.to_s
157-
)
158-
end
159-
160-
if national_reporting?
161-
attributes.merge!(
162-
local_patient_id: local_patient_id&.to_s,
163-
local_patient_id_uri: local_patient_id_uri&.to_s
164-
)
165-
end
166-
167-
attributes_to_stage_if_already_exists = {
168-
batch_number: batch_name&.to_s,
169-
batch_expiry: batch_expiry&.to_date,
170-
discarded_at: nil,
171-
notes: notes&.to_s,
172-
performed_at_time:,
173-
source:,
174-
vaccine_id: vaccine&.id
175-
}
176-
177-
delivery_attributes = {
178-
delivery_method: delivery_method_value,
179-
delivery_site: delivery_site_value
180-
}
181-
182121
vaccination_record =
183-
if national_reporting?
184-
VaccinationRecord.find_or_initialize_by(
185-
attributes.merge(
186-
attributes_to_stage_if_already_exists,
187-
delivery_attributes
188-
)
189-
)
190-
elsif uuid.present?
122+
if uuid.present?
191123
VaccinationRecord
192124
.find_by!(uuid: uuid.to_s)
193125
.tap { it.stage_changes(attributes) }
194126
else
195-
VaccinationRecord.find_or_initialize_by(attributes)
127+
VaccinationRecord.find_or_initialize_by(deduplication_attributes)
196128
end
197129

198130
if vaccination_record.persisted?
@@ -240,11 +172,94 @@ def to_archive_reason
240172
end
241173

242174
def set_patient(candidates: nil)
175+
# Invalidate the memoised `@attributes`, which might reference the old value of `@patient` (likely to be `nil`)
176+
@attributes = nil
177+
243178
@patient =
244179
existing_patients(candidates:)&.first ||
245180
Patient.new(new_patient_attributes)
246181
end
247182

183+
def attributes
184+
@attributes ||=
185+
begin
186+
outcome =
187+
(administered ? "administered" : reason_not_administered_value)
188+
189+
attributes = {
190+
disease_types:,
191+
dose_sequence: dose_sequence_value,
192+
full_dose: true,
193+
outcome:,
194+
patient:,
195+
performed_at_date:,
196+
performed_by_user:,
197+
performed_ods_code: performed_ods_code&.to_s,
198+
programme_type: programme&.type,
199+
protocol:,
200+
session:,
201+
supplied_by:
202+
}
203+
204+
attributes.merge!(location:, location_name:) unless imms_api_record?
205+
attributes.merge!(notify_parents: true) if session
206+
207+
if performed_by_user.nil? &&
208+
(
209+
performed_by_family_name.present? ||
210+
performed_by_given_name.present?
211+
)
212+
attributes.merge!(
213+
performed_by_family_name: performed_by_family_name&.to_s,
214+
performed_by_given_name: performed_by_given_name&.to_s
215+
)
216+
end
217+
218+
if national_reporting?
219+
attributes.merge!(
220+
local_patient_id: local_patient_id&.to_s,
221+
local_patient_id_uri: local_patient_id_uri&.to_s
222+
)
223+
end
224+
225+
attributes
226+
end
227+
end
228+
229+
def attributes_to_stage_if_already_exists
230+
@attributes_to_stage_if_already_exists ||= {
231+
batch_number: batch_name&.to_s,
232+
batch_expiry: batch_expiry&.to_date,
233+
discarded_at: nil,
234+
notes: notes&.to_s,
235+
performed_at_time:,
236+
source:,
237+
vaccine_id: vaccine&.id
238+
}
239+
end
240+
241+
def delivery_attributes
242+
@delivery_attributes ||= {
243+
delivery_method: delivery_method_value,
244+
delivery_site: delivery_site_value
245+
}
246+
end
247+
248+
def deduplication_attributes
249+
if national_reporting?
250+
attributes.merge(
251+
attributes_to_stage_if_already_exists,
252+
delivery_attributes
253+
)
254+
else
255+
attributes
256+
end
257+
end
258+
259+
def full_row_deduplication_attributes
260+
deduplication_attributes.merge(new_patient_attributes)
261+
end
262+
248263
def batch_expiry = @data[:batch_expiry_date]
249264

250265
def batch_name = @data[:batch_number] || @data[:vaccination_batch_number]
@@ -340,7 +355,7 @@ def location_name
340355
end
341356
end
342357

343-
def performed_at_date = date_of_vaccination.to_date
358+
def performed_at_date = date_of_vaccination&.to_date
344359

345360
def performed_at_time = time_of_vaccination&.to_time
346361

@@ -399,6 +414,18 @@ def session
399414
end
400415
end
401416

417+
def source
418+
if imms_api_record?
419+
"nhs_immunisations_api"
420+
elsif offline_recording?
421+
"service"
422+
elsif point_of_care?
423+
"historical_upload"
424+
else
425+
"national_reporting"
426+
end
427+
end
428+
402429
def protocol
403430
if imms_api_record?
404431
nil
@@ -450,7 +477,10 @@ def programmes_by_name
450477
end
451478
end
452479

453-
delegate :default_dose_sequence, :maximum_dose_sequence, to: :programme
480+
delegate :default_dose_sequence,
481+
:maximum_dose_sequence,
482+
to: :programme,
483+
allow_nil: true
454484

455485
def offline_recording? = session_id.present?
456486

@@ -546,7 +576,7 @@ def delivery_method_value
546576
end
547577
end
548578

549-
def disease_types = vaccine&.disease_types || programme.disease_types
579+
def disease_types = vaccine&.disease_types || programme&.disease_types
550580

551581
def dose_sequence_value
552582
value =

spec/factories/immunisation_imports.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
csv_filename { Faker::File.file_name(ext: "csv") }
4242
rows_count { rand(100..1000) }
4343

44-
type { "point_of_care" }
44+
type { team.type }
4545

4646
trait :csv_removed do
4747
csv_data { nil }

spec/factories/teams.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,13 @@
6060

6161
trait :national_reporting do
6262
type { :national_reporting }
63+
6364
email { nil }
6465
phone { nil }
6566
privacy_notice_url { nil }
6667
privacy_policy_url { nil }
68+
69+
programmes { [Programme.flu, Programme.hpv] }
6770
end
6871

6972
trait :with_one_nurse do

spec/features/import_vaccination_records_national_reporting_spec.rb

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,7 @@
5252
end
5353

5454
def given_mavis_logins_are_configured
55-
programmes = [Programme.flu, Programme.hpv]
56-
@team =
57-
create(
58-
:team,
59-
:national_reporting,
60-
:with_one_nurse,
61-
ods_code: "R1L",
62-
programmes: programmes
63-
)
55+
@team = create(:team, :national_reporting, :with_one_nurse, ods_code: "R1L")
6456
create(:school, team: @team, urn: 100_000)
6557
end
6658

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ORGANISATION_CODE,SCHOOL_URN,SCHOOL_NAME,NHS_NUMBER,PERSON_FORENAME,PERSON_SURNAME,PERSON_DOB,PERSON_GENDER_CODE,PERSON_POSTCODE,VACCINATED,DATE_OF_VACCINATION,VACCINE_GIVEN,BATCH_NUMBER,BATCH_EXPIRY_DATE,ANATOMICAL_SITE,PERFORMING_PROFESSIONAL_FORENAME,PERFORMING_PROFESSIONAL_SURNAME,REASON_NOT_VACCINATED,DOSE_SEQUENCE,CONSENT_TYPE,LOCAL_PATIENT_ID,LOCAL_PATIENT_ID_URI,CARE_SETTING
2+
RYG,100000,Hogwarts,9449308357,Harry,Potter,20010101,male,AA11 1AA,Y,20251109,AstraZeneca Fluenz LAIV,ABC123,20251030,nasal,Albus,Dumbledore,,,Parental Consent,1234,ABCD,
3+
RYG,100000,Hogwarts,9449308357,Harry,Potter,20010101,male,AA11 1AA,Y,20251109,AstraZeneca Fluenz LAIV,ABC123,20251030,nasal,Albus,Dumbledore,,,Parental Consent,1234,ABCD,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ORGANISATION_CODE,SCHOOL_URN,SCHOOL_NAME,NHS_NUMBER,PERSON_FORENAME,PERSON_SURNAME,PERSON_DOB,PERSON_GENDER_CODE,PERSON_POSTCODE,VACCINATED,DATE_OF_VACCINATION,PROGRAMME,VACCINE_GIVEN,BATCH_NUMBER,BATCH_EXPIRY_DATE,ANATOMICAL_SITE,PERFORMING_PROFESSIONAL_FORENAME,PERFORMING_PROFESSIONAL_SURNAME,REASON_NOT_VACCINATED,CONSENT_TYPE,LOCAL_PATIENT_ID,LOCAL_PATIENT_ID_URI
2+
R1L,120026,shaftesbury junior school ,7420180008,Chyna,Pickle,20120912,Not Specified,LE3 2DA,Yes,20250514,Flu,,123013325,20220730,Left Buttock,Vaccinator1,Name1,,Parental Consent,LocalPatient1,www.LocalPatient1
3+
R1L,120026,shaftesbury junior school ,7420180008,Chyna,Pickle,20120912,Not Specified,LE3 2DA,Yes,20250514,Flu,,ABC123,20231001,Left Buttock,Vaccinator1,Name1,,Parental Consent,LocalPatient1,www.LocalPatient1

0 commit comments

Comments
 (0)