Skip to content

Commit d00d8da

Browse files
Merge pull request #6548 from NHSDigital/alistair/imms-api-fix-duplicate-referencing
Fix referencing `duplicate_of_vaccination_record_id` after a second `Search...Job` run on the same records
2 parents b9d5bcc + d54b8da commit d00d8da

24 files changed

Lines changed: 311 additions & 88 deletions

app/jobs/search_vaccination_records_in_nhs_job.rb

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,23 @@ def perform(patient_id)
2020

2121
return unless feature_flags_enabled
2222

23-
existing_vaccination_records.find_each do |vaccination_record|
23+
existing_vaccination_records.each do |vaccination_record|
2424
incoming_vaccination_record =
2525
incoming_vaccination_records.find do
2626
it.nhs_immunisations_api_id ==
2727
vaccination_record.nhs_immunisations_api_id
2828
end
2929

3030
if incoming_vaccination_record
31-
vaccination_record.update!(
31+
vaccination_record.assign_attributes(
3232
incoming_vaccination_record
3333
.attributes
3434
.except("id", "uuid", "created_at")
35-
.merge(updated_at: Time.current)
35+
.merge(
36+
duplicate_of_vaccination_record:
37+
incoming_vaccination_record.duplicate_of_vaccination_record,
38+
updated_at: Time.current
39+
)
3640
)
3741

3842
incoming_vaccination_records.delete(incoming_vaccination_record)
@@ -41,10 +45,12 @@ def perform(patient_id)
4145
end
4246
end
4347

44-
# Remaining incoming_vaccination_records are new.
4548
# Save non-discarded records first so they have IDs before discarded
4649
# duplicates reference them via duplicate_of_vaccination_record_id.
47-
incoming_vaccination_records.sort_by { it.discarded? ? 1 : 0 }.each(&:save!)
50+
(
51+
existing_vaccination_records.reject(&:destroyed?) +
52+
incoming_vaccination_records
53+
).sort_by { it.discarded? ? 1 : 0 }.each(&:save!)
4854

4955
update_vaccination_search_timestamps if patient.nhs_number.present?
5056

@@ -104,6 +110,7 @@ def existing_vaccination_records
104110
@existing_vaccination_records ||=
105111
patient
106112
.vaccination_records
113+
.with_discarded
107114
.includes(:identity_check)
108115
.sourced_from_nhs_immunisations_api
109116
.for_programmes(programmes)
@@ -150,8 +157,23 @@ def deduplicate_vaccination_records(incoming_vaccination_records)
150157
end
151158
elsif records.any?(&:nhs_immunisations_api_primary_source)
152159
# If some records have `primarySource: true`, set `discarded_at` for all `primarySource: false` records,
153-
# pointing each at the first `primarySource: true` record
154-
canonical = records.find(&:nhs_immunisations_api_primary_source)
160+
# pointing each at the first `primarySource: true` record.
161+
162+
canonical_incoming =
163+
records.find(&:nhs_immunisations_api_primary_source)
164+
canonical_existing =
165+
patient
166+
.vaccination_records
167+
.sourced_from_nhs_immunisations_api
168+
.with_discarded
169+
.find_by(
170+
nhs_immunisations_api_id:
171+
canonical_incoming.nhs_immunisations_api_id
172+
)
173+
174+
# Prefer the persisted DB record (if it already exists from a previous run) so that
175+
# `duplicate_of_vaccination_record_id` is non-nil when the non-primary record is saved.
176+
canonical = canonical_existing || canonical_incoming
155177
records
156178
.select(&:sourced_from_nhs_immunisations_api?)
157179
.reject(&:nhs_immunisations_api_primary_source)

spec/fixtures/files/fhir/search_response_0_results.json renamed to spec/fixtures/files/fhir/search_responses/0_results.json

File renamed without changes.

spec/fixtures/files/fhir/search_response_1_result.json renamed to spec/fixtures/files/fhir/search_responses/1_result.json

File renamed without changes.

spec/fixtures/files/fhir/search_response_1_result_in_academic_year_2025.json renamed to spec/fixtures/files/fhir/search_responses/1_result_in_academic_year_2025.json

File renamed without changes.

spec/fixtures/files/fhir/search_response_1_result_mavis.json renamed to spec/fixtures/files/fhir/search_responses/1_result_mavis.json

File renamed without changes.

spec/fixtures/files/fhir/search_response_1_result_mmrv.json renamed to spec/fixtures/files/fhir/search_responses/1_result_mmrv.json

File renamed without changes.

spec/fixtures/files/fhir/search_response_1_result_old_date.json renamed to spec/fixtures/files/fhir/search_responses/1_result_old_date.json

File renamed without changes.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
{
2+
"type": "searchset",
3+
"total": 1,
4+
"link": [
5+
{
6+
"relation": "self",
7+
"url": "https://api.service.nhs.uk/immunisation-fhir-api/Immunization?-immunization.target=FLU,HPV,MENACWY,3IN1,MMR,MMRV&patient.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/322a54c7-acd8-4eb7-adbc-4006938df8f2",
13+
"resource": {
14+
"id": "322a54c7-acd8-4eb7-adbc-4006938df8f2",
15+
"extension": [
16+
{
17+
"url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-VaccinationProcedure",
18+
"valueCodeableConcept": {
19+
"coding": [
20+
{
21+
"system": "http://snomed.info/sct",
22+
"code": "884861000000100",
23+
"display": "Administration of first intranasal seasonal influenza vaccination"
24+
}
25+
]
26+
}
27+
}
28+
],
29+
"identifier": [
30+
{
31+
"use": "official",
32+
"system": "YGA",
33+
"value": "TPP_V_429814877"
34+
}
35+
],
36+
"status": "completed",
37+
"vaccineCode": {
38+
"coding": [
39+
{
40+
"system": "http://snomed.info/sct",
41+
"code": "46233009",
42+
"display": "Influenza vaccine"
43+
}
44+
]
45+
},
46+
"patient": {
47+
"reference": "urn:uuid:c3e7be44-bb52-4df7-8232-7500ed90c137",
48+
"type": "Patient",
49+
"identifier": {
50+
"system": "https://fhir.nhs.uk/Id/nhs-number",
51+
"value": "9449308357"
52+
}
53+
},
54+
"occurrenceDateTime": "2025-09-24T00:00:00+00:00",
55+
"recorded": "2025-10-02",
56+
"primarySource": false,
57+
"location": {
58+
"identifier": {
59+
"system": "https://fhir.nhs.uk/Id/ods-organization-code",
60+
"value": "100001"
61+
}
62+
},
63+
"lotNumber": "BU5086",
64+
"site": {
65+
"coding": [
66+
{
67+
"system": "http://snomed.info/sct",
68+
"code": "279549004",
69+
"display": "Nasal cavity structure"
70+
}
71+
]
72+
},
73+
"performer": [
74+
{
75+
"actor": {
76+
"type": "Organization",
77+
"identifier": {
78+
"system": "https://fhir.nhs.uk/Id/ods-organization-code",
79+
"value": "R1L"
80+
}
81+
}
82+
}
83+
],
84+
"protocolApplied": [
85+
{
86+
"targetDisease": [
87+
{
88+
"coding": [
89+
{
90+
"system": "http://snomed.info/sct",
91+
"code": "6142004",
92+
"display": "Influenza caused by seasonal influenza virus (disorder)"
93+
}
94+
]
95+
}
96+
],
97+
"doseNumberPositiveInt": 1
98+
}
99+
],
100+
"resourceType": "Immunization"
101+
},
102+
"search": {
103+
"mode": "match"
104+
}
105+
},
106+
{
107+
"fullUrl": "urn:uuid:c3e7be44-bb52-4df7-8232-7500ed90c137",
108+
"resource": {
109+
"id": "7101120547",
110+
"identifier": [
111+
{
112+
"system": "https://fhir.nhs.uk/Id/nhs-number",
113+
"value": "7101120547"
114+
}
115+
],
116+
"resourceType": "Patient"
117+
},
118+
"search": {
119+
"mode": "include"
120+
}
121+
}
122+
],
123+
"resourceType": "Bundle"
124+
}

spec/fixtures/files/fhir/search_response_2_results.json renamed to spec/fixtures/files/fhir/search_responses/2_results.json

File renamed without changes.

spec/fixtures/files/fhir/search_response_2_results_mavis_duplicate.json renamed to spec/fixtures/files/fhir/search_responses/2_results_mavis_duplicate.json

File renamed without changes.

0 commit comments

Comments
 (0)