From 85c18cae4fcdb274ca488221285536e3bdae319a Mon Sep 17 00:00:00 2001 From: Owajigbanam Ogbuluijah Date: Thu, 6 Aug 2026 11:13:52 +0100 Subject: [PATCH 1/2] fix: merge on duplicate encounters --- app/models/concerns/mergeable.rb | 25 ++++++++++++++++++++++- spec/models/concerns/mergeable_spec.rb | 28 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/app/models/concerns/mergeable.rb b/app/models/concerns/mergeable.rb index 1306b25dc8..8609072a39 100644 --- a/app/models/concerns/mergeable.rb +++ b/app/models/concerns/mergeable.rb @@ -75,11 +75,34 @@ def invalid_record(new_record) def create_new_record(attributes) logger.debug "#{self} with id #{attributes["id"]} is new, creating." - increment_metric(:new) record = create(attributes) + increment_metric(:new) record.merge_status = :new record + rescue ActiveRecord::RecordNotUnique + existing_record = with_discarded.find_by(id: attributes["id"]) + raise unless existing_record + + logger.debug "#{self} with id #{attributes["id"]} was created concurrently, merging instead." + merge_concurrent_record(existing_record, attributes) + end + + def merge_concurrent_record(existing_record, attributes) + new_record = new(attributes) + + case merge_status(new_record, existing_record) + when :discarded + discarded_record(existing_record) + when :updated + update_existing_record(existing_record, attributes) + when :identical + return_identical_record(existing_record) + when :old + return_old_record(existing_record) + else + raise ActiveRecord::RecordNotUnique + end end def update_existing_record(existing_record, attributes) diff --git a/spec/models/concerns/mergeable_spec.rb b/spec/models/concerns/mergeable_spec.rb index 0bc9d6a561..c4df489e2a 100644 --- a/spec/models/concerns/mergeable_spec.rb +++ b/spec/models/concerns/mergeable_spec.rb @@ -83,6 +83,34 @@ Patient.merge(updated_patient.attributes) end + it "merges an existing record when create races with a concurrent insert" do + facility = create(:facility) + patient = create(:patient, registration_facility: facility) + encountered_on = Date.new(2019, 1, 1) + encounter_id = Encounter.generate_id(facility.id, patient.id, encountered_on) + existing_encounter = create(:encounter, + id: encounter_id, + facility: facility, + patient: patient, + encountered_on: encountered_on) + newer_attributes = existing_encounter.attributes.merge( + "device_updated_at" => existing_encounter.device_updated_at + 1.hour + ) + + allow(Encounter).to receive(:create).and_raise( + ActiveRecord::RecordNotUnique.new( + 'duplicate key value violates unique constraint "encounters_pkey"' + ) + ) + + result = Encounter.merge(newer_attributes) + + expect(result.id).to eq(encounter_id) + expect(result.merge_status).to eq(:updated) + expect(Encounter.count).to eq(1) + expect(existing_encounter.reload.device_updated_at).to eq(newer_attributes["device_updated_at"]) + end + it "works for all models" do new_address = FactoryBot.build(:address) Address.merge(new_address.attributes) From 348ac7971ac2cf243fc894b2d4aebeb54f4ea2ce Mon Sep 17 00:00:00 2001 From: Owajigbanam Ogbuluijah Date: Thu, 6 Aug 2026 12:52:21 +0100 Subject: [PATCH 2/2] fix time precision --- spec/models/concerns/mergeable_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/models/concerns/mergeable_spec.rb b/spec/models/concerns/mergeable_spec.rb index c4df489e2a..a472a3ec35 100644 --- a/spec/models/concerns/mergeable_spec.rb +++ b/spec/models/concerns/mergeable_spec.rb @@ -108,7 +108,8 @@ expect(result.id).to eq(encounter_id) expect(result.merge_status).to eq(:updated) expect(Encounter.count).to eq(1) - expect(existing_encounter.reload.device_updated_at).to eq(newer_attributes["device_updated_at"]) + expect(existing_encounter.reload.device_updated_at.to_i) + .to eq(newer_attributes["device_updated_at"].to_i) end it "works for all models" do