Skip to content

Commit 5897d63

Browse files
authored
feat(OpenSRP): Deduplicator (#5677)
**Story card:** [sc-16598](https://app.shortcut.com/simpledotorg/story/16598/fix-import-api-duplications) ## Because The Import API wrongly duplicated records. ## This addresses Implemeting our [deduplication strategy](https://docs.google.com/document/d/15AYeR_qnYcT17Xbis_UaepVQJmgTtY5VWGhZp9x8b2w/edit?tab=t.0) ## Test instructions suite tests
1 parent 0976551 commit 5897d63

File tree

15 files changed

+407
-0
lines changed

15 files changed

+407
-0
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literals: true
2+
3+
module OneOff
4+
module Opensrp
5+
class ImportedDuplicatesQuery
6+
def self.call
7+
new.call
8+
end
9+
10+
def call
11+
query.map { |patient| [patient.id, patient.patient_id] }
12+
end
13+
14+
private
15+
16+
def query
17+
Patient
18+
.joins("inner join patient_business_identifiers on patient_business_identifiers.identifier = patients.id::text")
19+
.select("patients.id, patient_business_identifiers.patient_id")
20+
end
21+
end
22+
23+
class Deduplicator
24+
AFFECTED_ENTITIES = %w[
25+
Appointment
26+
BloodPressure
27+
BloodSugar
28+
MedicalHistory
29+
PrescriptionDrug
30+
Patient
31+
].freeze
32+
# The order is important here. Patient must be last.
33+
34+
def self.call!
35+
new.call!
36+
end
37+
38+
def initialize
39+
@duplicates = ImportedDuplicatesQuery.call
40+
end
41+
42+
def call!
43+
@duplicates.each do |old_id, new_id|
44+
AFFECTED_ENTITIES.each do |entity|
45+
deduplicator = [
46+
Module.nesting[1],
47+
"Deduplicators",
48+
"For#{entity}"
49+
].join("::").constantize
50+
deduplicator.call! old_id, new_id
51+
end
52+
end
53+
54+
deprecate_old_patients
55+
end
56+
57+
def deprecate_old_patients
58+
@old_patients = @duplicates.map(&:first)
59+
Patient.where(id: @old_patients).discard!
60+
end
61+
end
62+
end
63+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module OneOff
2+
module Opensrp
3+
module Deduplicators
4+
class ForAppointment < ForImmutableEntity
5+
def initialize old_id, new_id
6+
super old_id, new_id, :appointments
7+
end
8+
end
9+
end
10+
end
11+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module OneOff
2+
module Opensrp
3+
module Deduplicators
4+
class ForBloodPressure < ForImmutableEntity
5+
def initialize old_id, new_id
6+
super old_id, new_id, :blood_pressures
7+
end
8+
end
9+
end
10+
end
11+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module OneOff
2+
module Opensrp
3+
module Deduplicators
4+
class ForBloodSugar < ForImmutableEntity
5+
def initialize old_id, new_id
6+
super old_id, new_id, :blood_sugars
7+
end
8+
end
9+
end
10+
end
11+
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module OneOff
2+
module Opensrp
3+
module Deduplicators
4+
class ForEntity
5+
def self.call! old_id, new_id
6+
new(old_id, new_id).call
7+
end
8+
9+
def initialize old_id, new_id
10+
@old_id = old_id
11+
@new_id = new_id
12+
end
13+
14+
def new_patient
15+
@new_patient ||= Patient.find(@new_id)
16+
end
17+
18+
def old_patient
19+
@old_patient ||= Patient.find(@old_id)
20+
end
21+
22+
def call!
23+
merge.save!
24+
end
25+
26+
def merge
27+
raise "Unimplemented"
28+
end
29+
end
30+
end
31+
end
32+
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
module OneOff
2+
module Opensrp
3+
module Deduplicators
4+
class ForImmutableEntity < ForEntity
5+
def initialize old_id, new_id, assoc = nil
6+
super(old_id, new_id)
7+
8+
@association = assoc
9+
end
10+
11+
def merge
12+
old_patient.send(assoc).each do |associated|
13+
associated.patient = new_patient
14+
associated.save!
15+
end
16+
17+
# return the old patient object for the chain
18+
old_patient
19+
end
20+
end
21+
end
22+
end
23+
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module OneOff
2+
module Opensrp
3+
module Deduplicators
4+
class ForMedicalHistory < ForMutableEntity
5+
CHOOSING_NEW = %i[
6+
device_updated_at
7+
updated_at
8+
prior_heart_attack
9+
prior_stroke
10+
chronic_kidney_disease
11+
receiving_treatment_for_hypertension
12+
diabetes
13+
diagnosed_with_hypertension
14+
diagnosed_with_hypertension_boolean
15+
hypertension
16+
receiving_treatment_for_diabetes
17+
deleted_at
18+
].freeze
19+
20+
CHOOSING_OLD = %i[
21+
created_at
22+
device_created_at
23+
].freeze
24+
25+
def merge
26+
new_patient.medical_history.tap do |new_medical_history|
27+
merge_old(new_medical_history, old_patient.medical_history, CHOOSING_OLD)
28+
merge_new(new_medical_history, old_patient.medical_history, CHOOSING_NEW)
29+
end
30+
end
31+
end
32+
end
33+
end
34+
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module OneOff
2+
module Opensrp
3+
module Deduplicators
4+
class ForMutableEntity < ForEntity
5+
def merge
6+
raise "Unimplemented"
7+
end
8+
9+
def merge_non_null(entity, old_entity, attributes)
10+
attributes.each do |attr|
11+
val = [entity, old_entity].map { |p| p.send(attr) }.compact
12+
if val.size > 1
13+
@needs_manual_merge << attr
14+
next
15+
end
16+
entity.send("#{attr}=", val.first)
17+
end
18+
end
19+
20+
def merge_new(entity, old_patient, attributes)
21+
# no-op; since entity == new_entity
22+
end
23+
24+
def merge_old(entity, old_entity, attributes)
25+
attributes.each do |attr|
26+
entity.send("#{attr}=", old_entity.send(attr))
27+
end
28+
end
29+
end
30+
end
31+
end
32+
end
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
module OneOff
2+
module Opensrp
3+
module Deduplicators
4+
class ForPatient < ForMutableEntity
5+
# With these, we don't need to do anything because we are merging
6+
# forward. Since some of the attributes here are required from the API
7+
# spec, it is guaranteed to be present during the merge
8+
CHOOSING_NEW = %i[
9+
full_name
10+
updated_at
11+
gender
12+
status
13+
date_of_birth
14+
].freeze
15+
16+
# These are information we believe should be kept the same as the old
17+
# record for dsahboard purposes.
18+
CHOOSING_OLD = %i[
19+
assigned_facility_id
20+
created_at
21+
eligible_for_reassignment
22+
recorded_at
23+
registration_facility_id
24+
registration_user_id
25+
reminder_consent
26+
]
27+
28+
# These are the information which do not have real bearing on numbers,
29+
# but we need to merge them together to get a holistic patient story
30+
CHOOSING_NON_NULL = %i[
31+
address_id
32+
contacted_by_counsellor
33+
could_not_contact_reason
34+
deleted_at
35+
deleted_by_user_id
36+
deleted_reason
37+
]
38+
39+
def initialize old_id, new_id
40+
super(old_id, new_id)
41+
@needs_manual_merge = []
42+
end
43+
44+
def merge
45+
new_patient.tap do |patient|
46+
merge_old(patient, old_patient, CHOOSING_OLD)
47+
merge_non_null(patient, old_patient, CHOOSING_NON_NULL)
48+
49+
# For all which could not be merged automatically during the
50+
# non-null merge... perfer the newer value. — which is a no-op effectively
51+
merge_new(patient, old_patient, @needs_manual_merge)
52+
53+
# Merge age as a special case
54+
merge_age(patient)
55+
end
56+
end
57+
58+
private
59+
60+
def merge_age(patient)
61+
# Age consists of three columns: age, date_of_birth, and age_updated_at
62+
# For date_of_birth...
63+
# this is required from the API, so this prefers the new
64+
# For age
65+
# calculate this from the new date_of_birth
66+
# For age_updated_at
67+
# set this to now()
68+
69+
patient.age = Date.today.year - patient.date_of_birth.year
70+
patient.age_updated_at = Time.now
71+
end
72+
end
73+
end
74+
end
75+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module OneOff
2+
module Opensrp
3+
module Deduplicators
4+
class ForPrescriptionDrug < ForImmutableEntity
5+
def initialize old_id, new_id
6+
super old_id, new_id, :prescription_drugs
7+
end
8+
end
9+
end
10+
end
11+
end

0 commit comments

Comments
 (0)