dde_client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +37 -0
- data/Rakefile +12 -0
- data/app/assets/config/dde_client_manifest.js +1 -0
- data/app/assets/stylesheets/dde_client/application.css +15 -0
- data/app/controllers/dde_client/api/v1/dde_controller.rb +92 -0
- data/app/controllers/dde_client/api/v1/rollback_controller.rb +25 -0
- data/app/controllers/dde_client/application_controller.rb +4 -0
- data/app/helpers/dde/application_helper.rb +4 -0
- data/app/jobs/dde/application_job.rb +4 -0
- data/app/mailers/dde/application_mailer.rb +6 -0
- data/app/models/dde/application_record.rb +5 -0
- data/app/services/dde_client/dde_client.rb +162 -0
- data/app/services/dde_client/dde_service.rb +643 -0
- data/app/services/dde_client/matcher.rb +92 -0
- data/app/services/dde_client/merging_service.rb +769 -0
- data/app/services/dde_client/rollback_service.rb +320 -0
- data/app/services/merge_audit_service.rb +56 -0
- data/app/utils/model_utils.rb +62 -0
- data/app/views/layouts/dde/application.html.erb +15 -0
- data/config/routes.rb +14 -0
- data/lib/dde_client/client_error.rb +3 -0
- data/lib/dde_client/engine.rb +5 -0
- data/lib/dde_client/version.rb +5 -0
- data/lib/dde_client.rb +9 -0
- metadata +100 -0
@@ -0,0 +1,769 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# An extension to the DdeService that provides merging functionality
|
4
|
+
# for local patients and remote patients
|
5
|
+
class DdeClient::MergingService
|
6
|
+
include ModelUtils
|
7
|
+
|
8
|
+
attr_accessor :parent
|
9
|
+
|
10
|
+
# Initialise Dde's merging service.
|
11
|
+
#
|
12
|
+
# Parameters:
|
13
|
+
# parent: Is the parent Dde service
|
14
|
+
# dde_client: Is a configured Dde client
|
15
|
+
def initialize(parent, dde_client)
|
16
|
+
@parent = parent
|
17
|
+
@dde_client = dde_client
|
18
|
+
end
|
19
|
+
|
20
|
+
# Merge secondary patient(s) into primary patient.
|
21
|
+
#
|
22
|
+
# Parameters:
|
23
|
+
# primary_patient_ids - An object of them form { 'patient_id' => xxx, 'doc_id' }.
|
24
|
+
# One of 'patient_id' and 'doc_id' must be present else an
|
25
|
+
# InvalidParametersError be thrown.
|
26
|
+
# secondary_patient_ids_list - An array of objects like that for 'primary_patient_ids'
|
27
|
+
# above
|
28
|
+
def merge_patients(primary_patient_ids, secondary_patient_ids_list)
|
29
|
+
secondary_patient_ids_list.collect do |secondary_patient_ids|
|
30
|
+
if !dde_enabled?
|
31
|
+
merge_local_patients(primary_patient_ids, secondary_patient_ids, 'Local Patients')
|
32
|
+
elsif remote_merge?(primary_patient_ids, secondary_patient_ids)
|
33
|
+
merge_remote_patients(primary_patient_ids, secondary_patient_ids)
|
34
|
+
elsif remote_local_merge?(primary_patient_ids, secondary_patient_ids)
|
35
|
+
merge_remote_and_local_patients(primary_patient_ids, secondary_patient_ids, 'Remote and Local Patient')
|
36
|
+
elsif inverted_remote_local_merge?(primary_patient_ids, secondary_patient_ids)
|
37
|
+
merge_local_patients(primary_patient_ids, secondary_patient_ids, 'Local and Remote Patients')
|
38
|
+
elsif local_merge?(primary_patient_ids, secondary_patient_ids)
|
39
|
+
merge_local_patients(primary_patient_ids, secondary_patient_ids, 'Local Patients')
|
40
|
+
else
|
41
|
+
raise StandardError,
|
42
|
+
"Invalid merge parameters: primary => #{primary_patient_ids}, secondary => #{secondary_patient_ids}"
|
43
|
+
end
|
44
|
+
end.first
|
45
|
+
end
|
46
|
+
|
47
|
+
# Merges @{param secondary_patient} into @{param primary_patient}.
|
48
|
+
# rubocop:disable Metrics/MethodLength
|
49
|
+
# rubocop:disable Metrics/AbcSize
|
50
|
+
def merge_local_patients(primary_patient_ids, secondary_patient_ids, merge_type)
|
51
|
+
ActiveRecord::Base.transaction do
|
52
|
+
primary_patient = Patient.find(primary_patient_ids['patient_id'])
|
53
|
+
secondary_patient = Patient.find(secondary_patient_ids['patient_id'])
|
54
|
+
merge_name(primary_patient, secondary_patient)
|
55
|
+
merge_identifiers(primary_patient, secondary_patient)
|
56
|
+
merge_attributes(primary_patient, secondary_patient)
|
57
|
+
merge_address(primary_patient, secondary_patient)
|
58
|
+
@obs_map = {}
|
59
|
+
if female_male_merge?(primary_patient, secondary_patient) && secondary_female?(secondary_patient)
|
60
|
+
void_visit_type_encounter(primary_patient, secondary_patient, 'CxCa visit_type')
|
61
|
+
void_visit_type_encounter(primary_patient, secondary_patient, 'ANC PROGRAM')
|
62
|
+
end
|
63
|
+
result = merge_encounters(primary_patient, secondary_patient)
|
64
|
+
merge_observations(primary_patient, secondary_patient, result)
|
65
|
+
merge_orders(primary_patient, secondary_patient, result)
|
66
|
+
merge_visit_types(primary_patient, secondary_patient)
|
67
|
+
merge_patient_visits(primary_patient, secondary_patient)
|
68
|
+
DdeClient::MergeAuditService.new.create_merge_audit(primary_patient.id, secondary_patient.id, merge_type) if defined?(DdeClient::MergeAuditService)
|
69
|
+
secondary_patient.void("Merged into patient ##{primary_patient.id}:0")
|
70
|
+
|
71
|
+
primary_patient
|
72
|
+
end
|
73
|
+
end
|
74
|
+
# rubocop:enable Metrics/AbcSize
|
75
|
+
# rubocop:enable Metrics/MethodLength
|
76
|
+
|
77
|
+
# Binds the remote patient to the local patient by blessing the local patient
|
78
|
+
# with the remotes npid and doc_id
|
79
|
+
def link_local_to_remote_patient(local_patient, remote_patient)
|
80
|
+
return local_patient if local_patient_linked_to_remote?(local_patient, remote_patient)
|
81
|
+
|
82
|
+
national_id_type = patient_identifier_type('National id')
|
83
|
+
old_identifier = patient_identifier_type('Old Identification Number')
|
84
|
+
doc_id_type = patient_identifier_type('Dde person document id')
|
85
|
+
|
86
|
+
local_patient.patient_identifiers.where(type: [national_id_type, doc_id_type, old_identifier]).each do |identifier|
|
87
|
+
# We are now voiding all ids
|
88
|
+
# if identifier.identifier_type == national_id_type.id && identifier.identifier.match?(/^\s*P\d{12}\s*$/i)
|
89
|
+
# # We have a v3 NPID that should get demoted to legacy national id
|
90
|
+
# create_local_patient_identifier(local_patient, identifier.identifier, 'Old Identification Number')
|
91
|
+
# end
|
92
|
+
|
93
|
+
identifier.void("Assigned new id: #{remote_patient['doc_id']}")
|
94
|
+
end
|
95
|
+
|
96
|
+
create_local_patient_identifier(local_patient, remote_patient['doc_id'], 'Dde person document id')
|
97
|
+
create_local_patient_identifier(local_patient, find_remote_patient_npid(remote_patient), 'National id')
|
98
|
+
|
99
|
+
local_patient.reload
|
100
|
+
local_patient
|
101
|
+
end
|
102
|
+
|
103
|
+
def local_patient_linked_to_remote?(local_patient, remote_patient)
|
104
|
+
identifier_exists = lambda do |type, value|
|
105
|
+
PatientIdentifier.where(patient: local_patient, identifier_type: PatientIdentifierType.where(name: type), identifier: value)
|
106
|
+
.exists?
|
107
|
+
end
|
108
|
+
|
109
|
+
identifier_exists['National id',
|
110
|
+
remote_patient['npid']] && identifier_exists['Dde person document id', remote_patient['doc_id']]
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
# Checks whether the passed parameters are enough for a remote merge.
|
116
|
+
#
|
117
|
+
# The precondition for a remote merge is the presence of a doc_id
|
118
|
+
# in both primary and secondary patient ids.
|
119
|
+
def remote_merge?(primary_patient_ids, secondary_patient_ids)
|
120
|
+
!search_by_doc_id(primary_patient_ids['doc_id']).blank? && !search_by_doc_id(secondary_patient_ids['doc_id']).blank?
|
121
|
+
end
|
122
|
+
|
123
|
+
# Is a merge of a remote patient into a local patient possible?
|
124
|
+
def remote_local_merge?(primary_patient_ids, secondary_patient_ids)
|
125
|
+
!primary_patient_ids['patient_id'].blank? && !search_by_doc_id(secondary_patient_ids['doc_id']).blank?
|
126
|
+
end
|
127
|
+
|
128
|
+
# Like `remote_local_merge` but primary is remote and secondary is local
|
129
|
+
def inverted_remote_local_merge?(primary_patient_ids, secondary_patient_ids)
|
130
|
+
!search_by_doc_id(primary_patient_ids['doc_id']).blank? && !secondary_patient_ids['patient_id'].blank?
|
131
|
+
end
|
132
|
+
|
133
|
+
# Is a merge of local patients possible?
|
134
|
+
def local_merge?(primary_patient_ids, secondary_patient_ids)
|
135
|
+
!primary_patient_ids['patient_id'].blank? && !secondary_patient_ids['patient_id'].blank?
|
136
|
+
end
|
137
|
+
|
138
|
+
def search_by_doc_id(doc_id)
|
139
|
+
return nil if doc_id.blank?
|
140
|
+
|
141
|
+
response, status = dde_client.post('search_by_doc_id', doc_id: doc_id)
|
142
|
+
return nil unless status == 200
|
143
|
+
|
144
|
+
response
|
145
|
+
end
|
146
|
+
|
147
|
+
# Merge remote secondary patient into local primary patient
|
148
|
+
def merge_remote_and_local_patients(primary_patient_ids, secondary_patient_ids, merge_type)
|
149
|
+
local_patient = Patient.find(primary_patient_ids['patient_id'])
|
150
|
+
remote_patient = reassign_remote_patient_npid(secondary_patient_ids['doc_id'])
|
151
|
+
|
152
|
+
local_patient = link_local_to_remote_patient(local_patient, remote_patient)
|
153
|
+
return local_patient if secondary_patient_ids['patient_id'].blank?
|
154
|
+
|
155
|
+
merge_local_patients(primary_patient_ids, secondary_patient_ids, merge_type)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Merge remote secondary patient into local primary patient
|
159
|
+
def merge_local_and_remote_patients(primary_patient_ids, secondary_patient_ids, merge_type)
|
160
|
+
merge_local_patients(primary_patient_ids, secondary_patient_ids, merge_type)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Merge patients in Dde and update local records if need be
|
164
|
+
def merge_remote_patients(primary_patient_ids, secondary_patient_ids)
|
165
|
+
response, status = dde_client.post('merge_people', primary_person_doc_id: primary_patient_ids['doc_id'],
|
166
|
+
secondary_person_doc_id: secondary_patient_ids['doc_id'])
|
167
|
+
|
168
|
+
raise "Failed to merge patients on remote: #{status} - #{response}" unless status == 200
|
169
|
+
|
170
|
+
return parent.save_remote_patient(response) if primary_patient_ids['patient_id'].blank?
|
171
|
+
|
172
|
+
local_patient = link_local_to_remote_patient(Patient.find(primary_patient_ids['patient_id']), response)
|
173
|
+
return local_patient if secondary_patient_ids['patient_id'].blank?
|
174
|
+
|
175
|
+
merge_local_patients(local_patient, Patient.find(secondary_patient_ids['patient_id']), 'Remote Patients')
|
176
|
+
end
|
177
|
+
|
178
|
+
def create_local_patient_identifier(patient, value, type_name)
|
179
|
+
identifier = PatientIdentifier.create(identifier: value,
|
180
|
+
identifier_type: patient_identifier_type(type_name),
|
181
|
+
location_id: Location.current_location.id,
|
182
|
+
patient: patient,
|
183
|
+
preferred: 1)
|
184
|
+
return patient.reload && identifier if identifier.errors.empty?
|
185
|
+
|
186
|
+
raise "Could not save Dde identifier: #{type_name} due to #{identifier.errors.as_json}"
|
187
|
+
end
|
188
|
+
|
189
|
+
# Patch primary_patient missing name data using secondary_patient
|
190
|
+
def merge_name(primary_patient, secondary_patient)
|
191
|
+
|
192
|
+
raise "Primary Patient not found" if primary_patient.nil?
|
193
|
+
|
194
|
+
raise "Secondary Patient not found" if secondary_patient.nil?
|
195
|
+
|
196
|
+
primary_name = primary_patient.person.names.first
|
197
|
+
secondary_name = secondary_patient.person.names.first
|
198
|
+
|
199
|
+
return unless secondary_name
|
200
|
+
|
201
|
+
secondary_name_hash = secondary_name.as_json
|
202
|
+
|
203
|
+
# primary patient doesn't have a name, so just copy secondary patient's
|
204
|
+
unless primary_name
|
205
|
+
secondary_name_hash.delete('uuid')
|
206
|
+
secondary_name_hash.delete('person_name_id')
|
207
|
+
secondary_name_hash.delete('creator')
|
208
|
+
secondary_name_hash['person_id'] = primary_patient.patient_id
|
209
|
+
primary_name = PersonName.create(secondary_name_hash)
|
210
|
+
raise "Could not merge patient name: #{primary_name.errors.as_json}" unless primary_name.errors.empty?
|
211
|
+
|
212
|
+
secondary_name.void("Merged into patient ##{primary_patient.patient_id}:#{primary_name.id}")
|
213
|
+
return
|
214
|
+
end
|
215
|
+
|
216
|
+
params = primary_name.as_json.each_with_object({}) do |(field, value), params|
|
217
|
+
secondary_value = secondary_name_hash[field]
|
218
|
+
|
219
|
+
next unless value.blank? && !secondary_value.blank?
|
220
|
+
|
221
|
+
params[field] = secondary_value
|
222
|
+
end
|
223
|
+
|
224
|
+
primary_name.update(params)
|
225
|
+
secondary_name.void("Merged into patient ##{primary_patient.patient_id}:0")
|
226
|
+
end
|
227
|
+
|
228
|
+
NATIONAL_ID_TYPE = PatientIdentifierType.find_by_name!('National ID')
|
229
|
+
ARV_NUMBER_TYPE = PatientIdentifierType.find_by_name!('ARV Number')
|
230
|
+
LEGACY_ARV_NUMBER_TYPE = PatientIdentifierType.find_by_name!('Legacy ARV Number')
|
231
|
+
OLD_NATIONAL_ID_TYPE = PatientIdentifierType.find_by_name!('Old Identification Number')
|
232
|
+
|
233
|
+
# Bless primary_patient with identifiers available only to the secondary patient
|
234
|
+
def merge_identifiers(primary_patient, secondary_patient)
|
235
|
+
secondary_patient.patient_identifiers.each do |identifier|
|
236
|
+
next if patient_has_identifier(primary_patient, identifier.identifier_type, identifier.identifier)
|
237
|
+
|
238
|
+
new_identifier = PatientIdentifier.create!(
|
239
|
+
patient_id: primary_patient.patient_id,
|
240
|
+
location_id: identifier.location_id,
|
241
|
+
identifier: identifier.identifier,
|
242
|
+
identifier_type: if identifier.identifier_type == NATIONAL_ID_TYPE.id
|
243
|
+
# Can't have two National Patient IDs, the secondary ones are treated as old identifiers
|
244
|
+
OLD_NATIONAL_ID_TYPE.id
|
245
|
+
elsif identifier.identifier_type == ARV_NUMBER_TYPE.id
|
246
|
+
# Can't have two ARV numbers, the secondary ones are treated as legacy ARV numbers
|
247
|
+
LEGACY_ARV_NUMBER_TYPE.id
|
248
|
+
else
|
249
|
+
identifier.identifier_type
|
250
|
+
end
|
251
|
+
)
|
252
|
+
raise "Could not merge patient identifier: #{new_identifier.errors.as_json}" unless new_identifier.errors.empty?
|
253
|
+
|
254
|
+
new_id = new_identifier.id
|
255
|
+
Rails.logger.info "Patient ##{primary_patient.patient_id} has new identifier on ##{new_id}"
|
256
|
+
identifier.update(void_reason: "Merged into patient ##{primary_patient.patient_id}: #{new_id}", voided: 1,
|
257
|
+
date_voided: Time.now, voided_by: User.current.id)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def patient_has_identifier(patient, identifier_type_id, identifier_value)
|
262
|
+
patient.patient_identifiers
|
263
|
+
.where(identifier_type: identifier_type_id, identifier: identifier_value)
|
264
|
+
.exists?
|
265
|
+
end
|
266
|
+
|
267
|
+
# Patch primary_patient missing attributes using secondary patient data
|
268
|
+
def merge_attributes(primary_patient, secondary_patient)
|
269
|
+
secondary_patient.person.person_attributes.each do |attribute|
|
270
|
+
next if primary_patient.person.person_attributes.where(
|
271
|
+
person_attribute_type_id: attribute.person_attribute_type_id
|
272
|
+
).exists?
|
273
|
+
|
274
|
+
new_attribute = PersonAttribute.create(person_id: primary_patient.patient_id,
|
275
|
+
person_attribute_type_id: attribute.person_attribute_type_id,
|
276
|
+
value: attribute.value)
|
277
|
+
raise "Could not merge patient attribute: #{new_attribute.errors.as_json}" unless new_attribute.errors.empty?
|
278
|
+
|
279
|
+
attribute.void("Merged into patient ##{primary_patient.patient_id}:#{new_attribute.id}")
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Patch primary missing patient address data using from secondary patient address
|
284
|
+
def merge_address(primary_patient, secondary_patient)
|
285
|
+
primary_address = primary_patient.person.addresses.first
|
286
|
+
secondary_address = secondary_patient.person.addresses.first
|
287
|
+
|
288
|
+
return unless secondary_address
|
289
|
+
|
290
|
+
secondary_address_hash = secondary_address.as_json
|
291
|
+
|
292
|
+
unless primary_address
|
293
|
+
secondary_address_hash.delete('uuid')
|
294
|
+
secondary_address_hash.delete('person_address_id')
|
295
|
+
secondary_address_hash.delete('creator')
|
296
|
+
secondary_address_hash['person_id'] = primary_patient.patient_id
|
297
|
+
primary_address = PersonAddress.create(secondary_address_hash)
|
298
|
+
raise "Could not merge patient address: #{primary_address.errors.as_json}" unless primary_address.errors.empty?
|
299
|
+
|
300
|
+
secondary_address.void("Merged into patient ##{primary_patient.patient_id}:#{primary_address.id}")
|
301
|
+
return
|
302
|
+
end
|
303
|
+
|
304
|
+
params = primary_address.as_json.each_with_object({}) do |(field, value), params|
|
305
|
+
secondary_value = secondary_address_hash[field]
|
306
|
+
|
307
|
+
next unless value.blank? && !secondary_value.blank?
|
308
|
+
|
309
|
+
params[field] = secondary_value
|
310
|
+
end
|
311
|
+
|
312
|
+
primary_address.update(params)
|
313
|
+
secondary_address.void("Merged into patient ##{primary_patient.patient_id}:0")
|
314
|
+
end
|
315
|
+
|
316
|
+
# Strips off secondary_patient all orders and blesses primary patient
|
317
|
+
# with them
|
318
|
+
def merge_orders(primary_patient, secondary_patient, encounter_map)
|
319
|
+
Rails.logger.debug("Merging patient orders: #{primary_patient} <= #{secondary_patient}")
|
320
|
+
orders_map = {}
|
321
|
+
Order.where(patient_id: secondary_patient.id).each do |order|
|
322
|
+
check = Order.find_by('order_type_id = ? AND concept_id = ? AND patient_id = ? AND DATE(start_date) = ?',
|
323
|
+
order.order_type_id, order.concept_id, primary_patient.id, order.start_date.strftime('%Y-%m-%d'))
|
324
|
+
if check.blank?
|
325
|
+
primary_order_hash = order.attributes
|
326
|
+
primary_order_hash.delete('order_id')
|
327
|
+
primary_order_hash.delete('uuid')
|
328
|
+
primary_order_hash.delete('creator')
|
329
|
+
primary_order_hash.delete('order_id')
|
330
|
+
primary_order_hash['patient_id'] = primary_patient.id
|
331
|
+
primary_order_hash['encounter_id'] = encounter_map[order.encounter_id]
|
332
|
+
primary_order_hash['obs_id'] = @obs_map[order.obs_id] unless order.obs_id.blank?
|
333
|
+
primary_order = Order.create!(primary_order_hash)
|
334
|
+
raise "Could not merge patient orders: #{primary_order.errors.as_json}" unless primary_order.errors.empty?
|
335
|
+
|
336
|
+
create_new_drug_order(order, primary_order)
|
337
|
+
order.void("Merged into patient ##{primary_patient.patient_id}:#{primary_order.id}")
|
338
|
+
orders_map[order.id] = primary_order.id
|
339
|
+
else
|
340
|
+
create_new_drug_order(order, check) if order.drug_order.present? && check.drug_order.blank?
|
341
|
+
order.void("Merged into patient ##{primary_patient.patient_id}:0")
|
342
|
+
orders_map[order.id] = check.id
|
343
|
+
end
|
344
|
+
end
|
345
|
+
update_obs_order_id(orders_map, @obs_map)
|
346
|
+
end
|
347
|
+
|
348
|
+
def create_new_drug_order(order, primary_order)
|
349
|
+
return unless order.drug_order
|
350
|
+
return if primary_order.drug_order
|
351
|
+
|
352
|
+
Rails.logger.debug("Merging patient drug orders: #{primary_order} <= #{order}")
|
353
|
+
drug_order_hash = order.drug_order.attributes
|
354
|
+
drug_order_hash.delete('order_id')
|
355
|
+
|
356
|
+
drug_order_hash['order_id'] = primary_order.id
|
357
|
+
drug_order = DrugOrder.create!(drug_order_hash)
|
358
|
+
raise "Could not merge patient drug orders: #{drug_order.errors.as_json}" unless drug_order.errors.empty?
|
359
|
+
end
|
360
|
+
|
361
|
+
def merge_patient_visits(primary_patient, secondary_patient)
|
362
|
+
return unless defined?(Visit)
|
363
|
+
|
364
|
+
Rails.logger.debug("Merging patient visits: #{primary_patient} <= #{secondary_patient}")
|
365
|
+
|
366
|
+
Visit.where(patient_id: secondary_patient.id).each do |visit|
|
367
|
+
primary_visit_hash = visit.attributes
|
368
|
+
primary_visit_hash.delete('visit_id')
|
369
|
+
primary_visit_hash.delete('uuid')
|
370
|
+
primary_visit_hash.delete('creator')
|
371
|
+
primary_visit_hash['patient_id'] = primary_patient.id
|
372
|
+
primary_visit = Visit.create!(primary_visit_hash)
|
373
|
+
raise "Could not merge patient visits: #{primary_visit.errors.as_json}" unless primary_visit.errors.empty?
|
374
|
+
|
375
|
+
update_encounters_visit_id(new_visit: primary_visit, old_visit_id: visit.id)
|
376
|
+
|
377
|
+
visit.void("Merged into patient ##{primary_patient.patient_id}:#{primary_visit.id}")
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def update_encounters_visit_id(new_visit:, old_visit_id:)
|
382
|
+
Encounter.unscoped.where(visit_id: old_visit_id, patient: new_visit.patient_id).each do |encounter|
|
383
|
+
encounter.update(visit: new_visit)
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
# strip off secondary_patient all visit_type enrollments and blesses primary patient
|
388
|
+
# with them
|
389
|
+
def merge_visit_types(primary_patient, secondary_patient)
|
390
|
+
return unless defined?(PatientVisitType)
|
391
|
+
|
392
|
+
Rails.logger.debug("Merging patient visit_types: #{primary_patient} <= #{secondary_patient}")
|
393
|
+
PatientVisitType.where(patient_id: secondary_patient.id).each do |visit_type|
|
394
|
+
patient_states = visit_type.patient_states
|
395
|
+
check = PatientVisitType.find_by('visit_type_id = ? AND patient_id = ?', visit_type.visit_type_id, primary_patient.id)
|
396
|
+
if check.blank?
|
397
|
+
primary_visit_type_hash = visit_type.attributes
|
398
|
+
primary_visit_type_hash.delete('patient_visit_type_id')
|
399
|
+
primary_visit_type_hash.delete('uuid')
|
400
|
+
|
401
|
+
primary_visit_type_hash['patient_id'] = primary_patient.id
|
402
|
+
primary_visit_type = PatientVisitType.create!(primary_visit_type_hash)
|
403
|
+
raise "Could not merge patient visit_types: #{primary_visit_type.errors.as_json}" unless primary_visit_type.errors.empty?
|
404
|
+
|
405
|
+
merge_states(primary_visit_type, patient_states)
|
406
|
+
visit_type.void("Merged into patient ##{primary_patient.patient_id}:#{primary_visit_type.id}")
|
407
|
+
else
|
408
|
+
merge_states(check, patient_states)
|
409
|
+
visit_type.void("Merged into patient ##{primary_patient.patient_id}:0")
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
# strip off secondary_patient all pateint states and blesses primary patient
|
415
|
+
# with them
|
416
|
+
def merge_states(primary_patient_visit_type, secondary_patient_states)
|
417
|
+
return if secondary_patient_states.blank?
|
418
|
+
|
419
|
+
Rails.logger.debug("Merging patient states: #{primary_patient_visit_type.patient_id} <= #{secondary_patient_states[0].patient_visit_type.patient_id}")
|
420
|
+
secondary_patient_states.each do |state|
|
421
|
+
check = PatientState.find_by('patient_visit_type_id = ? AND state = ? AND DATE(start_date) = ?', primary_patient_visit_type.id, state.state, state.start_date.strftime('%Y-%m-%d'))
|
422
|
+
next unless check.blank?
|
423
|
+
|
424
|
+
primary_state_hash = state.attributes
|
425
|
+
primary_state_hash.delete('patient_state_id')
|
426
|
+
primary_state_hash.delete('uuid')
|
427
|
+
|
428
|
+
primary_state_hash['patient_visit_type_id'] = primary_patient_visit_type.id
|
429
|
+
patient_state = PatientState.create!(primary_state_hash)
|
430
|
+
raise "Could not merge patient states: #{patient_state.errors.as_json}" unless patient_state.errors.empty?
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
|
435
|
+
# method to update drug orders with the new order id
|
436
|
+
# def manage_drug_order(order_map)
|
437
|
+
# Rails.logger.debug("Merging patient drug orders: #{order_map}")
|
438
|
+
# return if order_map.blank?
|
439
|
+
|
440
|
+
# result = ActiveRecord::Base.connection.select_all "SELECT * FROM drug_order WHERE order_id IN (#{order_map.keys.join(',')})"
|
441
|
+
# return if result.blank?
|
442
|
+
|
443
|
+
# result.to_a.each do |drug_order|
|
444
|
+
# new_id = order_map[drug_order['order_id']]
|
445
|
+
# next if DrugOrder.where(order_id: new_id).exists?
|
446
|
+
|
447
|
+
# drug_order['order_id'] = new_id
|
448
|
+
# new_drug_order = DrugOrder.create!(drug_order)
|
449
|
+
# debugger
|
450
|
+
# raise "Could not merge patient druge orders: #{new_drug_order.errors.as_json}" unless new_drug_order.errors.empty?
|
451
|
+
# end
|
452
|
+
# end
|
453
|
+
|
454
|
+
def update_obs_order_id(order_map, obs_map)
|
455
|
+
Observation.where(obs_id: obs_map.values).each do |obs|
|
456
|
+
obs.update(order_id: order_map[obs.order_id]) unless obs.order_id.blank?
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
# Strips off secondary_patient all observations and blesses primary patient
|
461
|
+
# with them
|
462
|
+
def merge_observations(primary_patient, secondary_patient, encounter_map)
|
463
|
+
Rails.logger.debug("Merging patient observations: #{primary_patient} <= #{secondary_patient}")
|
464
|
+
|
465
|
+
Observation.where(person_id: secondary_patient.id).each do |obs|
|
466
|
+
check = Observation.find_by("person_id = #{primary_patient.id} AND concept_id = #{obs.concept_id} AND
|
467
|
+
DATE(obs_datetime) = DATE('#{obs.obs_datetime.strftime('%Y-%m-%d')}') #{unless obs.value_coded.blank?
|
468
|
+
"AND value_coded IS NOT NULL"
|
469
|
+
end} #{unless obs.obs_group_id.blank?
|
470
|
+
'AND obs_group_id IS NOT NULL'
|
471
|
+
end} #{unless obs.order_id.blank?
|
472
|
+
'AND order_id IS NOT NULL'
|
473
|
+
end}")
|
474
|
+
if check.blank?
|
475
|
+
primary_obs = process_obervation_merging(obs, primary_patient, encounter_map, secondary_patient)
|
476
|
+
@obs_map[obs.id] = primary_obs.id if primary_obs
|
477
|
+
else
|
478
|
+
obs.update(void_reason: "Merged into patient ##{primary_patient.patient_id}:0", voided: 1,
|
479
|
+
date_voided: Time.now, voided_by: User.current.id)
|
480
|
+
@obs_map[obs.id] = check.id
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
update_observations_group_id @obs_map
|
485
|
+
end
|
486
|
+
|
487
|
+
# method to check whether to add observations
|
488
|
+
def check_clinician?(provider)
|
489
|
+
User.find(provider).roles.map { |role| role['role'] }.include? 'Clinician'
|
490
|
+
end
|
491
|
+
|
492
|
+
# central place to void and create new observation
|
493
|
+
def process_obervation_merging(obs, primary_patient, encounter_map, secondary_patient)
|
494
|
+
if female_male_merge?(primary_patient,
|
495
|
+
secondary_patient) && secondary_female?(secondary_patient) && female_obs?(obs)
|
496
|
+
obs.void("Merged into patient ##{primary_patient.patient_id}:0")
|
497
|
+
return nil
|
498
|
+
end
|
499
|
+
primary_obs_hash = obs.attributes
|
500
|
+
primary_obs_hash.delete('obs_id')
|
501
|
+
primary_obs_hash.delete('uuid')
|
502
|
+
primary_obs_hash.delete('creator')
|
503
|
+
primary_obs_hash.delete('obs_id')
|
504
|
+
primary_obs_hash['encounter_id'] = encounter_map[obs.encounter_id]
|
505
|
+
primary_obs_hash['person_id'] = primary_patient.id
|
506
|
+
primary_obs = Observation.create(primary_obs_hash)
|
507
|
+
raise "Could not merge patient observations: #{primary_obs.errors.as_json}" unless primary_obs.errors.empty?
|
508
|
+
|
509
|
+
obs.update(void_reason: "Merged into patient ##{primary_patient.id}:#{primary_obs.id}", voided: 1,
|
510
|
+
date_voided: Time.now, voided_by: User.current.id)
|
511
|
+
primary_obs
|
512
|
+
end
|
513
|
+
|
514
|
+
# this method updates observation table group id on the newly created observation
|
515
|
+
def update_observations_group_id(obs_map)
|
516
|
+
Observation.where(obs_id: obs_map.values).limit(nil).each do |obs|
|
517
|
+
obs.update(obs_group_id: obs_map[obs.obs_group_id]) unless obs.obs_group_id.blank?
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
# Get all encounter types that involve referring to a clinician
|
522
|
+
def refer_to_clinician_encounter_types
|
523
|
+
@refer_to_clinician_encounter_types ||= EncounterType.where('name = ? OR name = ?', 'HIV CLINIC CONSULTATION',
|
524
|
+
'HYPERTENSION MANAGEMENT').map(&:encounter_type_id)
|
525
|
+
end
|
526
|
+
|
527
|
+
# Strips off secondary_patient all encounters and blesses primary patient
|
528
|
+
# with them
|
529
|
+
def merge_encounters(primary_patient, secondary_patient)
|
530
|
+
Rails.logger.debug("Merging patient encounters: #{primary_patient} <= #{secondary_patient}")
|
531
|
+
encounter_map = {}
|
532
|
+
|
533
|
+
# first get all encounter to be voided, create new instances from them, then void the encounter
|
534
|
+
Encounter.where(patient_id: secondary_patient.id).each do |encounter|
|
535
|
+
check = Encounter.find_by(
|
536
|
+
'patient_id = ? AND encounter_type = ? AND DATE(encounter_datetime) = DATE(?) AND visit_id = ?', primary_patient.id, encounter.encounter_type, encounter.encounter_datetime.to_date, encounter.visit_id
|
537
|
+
)
|
538
|
+
if check.blank?
|
539
|
+
encounter_map[encounter.id] = create_new_encounter(encounter, primary_patient)
|
540
|
+
else
|
541
|
+
encounter_map[encounter.id] = check.id
|
542
|
+
# we are trying to processes all clinician encounters observations if this visit resulted in being referred to the clinician
|
543
|
+
# the merging needs to be smart enough to include the observartions under clinician
|
544
|
+
if refer_to_clinician_encounter_types.include? encounter.encounter_type
|
545
|
+
process_encounter_obs(encounter, primary_patient, secondary_patient,
|
546
|
+
encounter_map)
|
547
|
+
end
|
548
|
+
encounter.update(void_reason: "Merged into patient ##{primary_patient.patient_id}:0", voided: 1,
|
549
|
+
date_voided: Time.now, voided_by: User.current.id)
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
encounter_map
|
554
|
+
end
|
555
|
+
|
556
|
+
def create_new_encounter(encounter, primary_patient)
|
557
|
+
return create_new_encounter_raw(encounter, primary_patient) if encounter.visit_id.blank?
|
558
|
+
|
559
|
+
primary_encounter_hash = encounter.attributes
|
560
|
+
primary_encounter_hash.delete('encounter_id')
|
561
|
+
primary_encounter_hash.delete('uuid')
|
562
|
+
primary_encounter_hash.delete('creator')
|
563
|
+
id = primary_encounter_hash.delete('encounter_type')
|
564
|
+
primary_encounter_hash['patient_id'] = primary_patient.id
|
565
|
+
|
566
|
+
primary_encounter = Encounter.new(primary_encounter_hash)
|
567
|
+
primary_encounter.encounter_type = EncounterType.find(id)
|
568
|
+
primary_encounter.save!
|
569
|
+
|
570
|
+
unless primary_encounter.errors.empty?
|
571
|
+
raise "Could not merge patient encounters: #{primary_encounter.errors.as_json}"
|
572
|
+
end
|
573
|
+
|
574
|
+
common_encounter_void(encounter, primary_patient, primary_encounter.id)
|
575
|
+
primary_encounter.id
|
576
|
+
end
|
577
|
+
|
578
|
+
def create_new_encounter_raw(encounter, primary_patient)
|
579
|
+
ActiveRecord::Base.connection.disable_referential_integrity do
|
580
|
+
puts "This is the encounter_id: #{encounter.id}"
|
581
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
582
|
+
INSERT INTO encounter (encounter_type, patient_id, encounter_datetime, provider_id,#{unless encounter.location.blank?
|
583
|
+
'location_id,'
|
584
|
+
end} #{unless encounter.form_id.blank?
|
585
|
+
'form_id,'
|
586
|
+
end} uuid, creator, date_created, voided #{unless encounter.changed_by.blank?
|
587
|
+
', changed_by'
|
588
|
+
end} #{unless encounter.date_changed.blank?
|
589
|
+
', date_changed'
|
590
|
+
end})
|
591
|
+
VALUES (#{encounter.encounter_type}, #{primary_patient.id}, '#{encounter.encounter_datetime.strftime('%Y-%m-%d %H:%M:%S')}', #{encounter.provider_id}, #{unless encounter.location_id.blank?
|
592
|
+
"#{encounter.location_id},"
|
593
|
+
end} #{unless encounter.form_id.blank?
|
594
|
+
"#{encounter.form_id},"
|
595
|
+
end} uuid(), #{User.current.id}, '#{encounter.date_created.strftime('%Y-%m-%d %H:%M:%S')}', #{encounter.voided} #{",{encounter.changed_by}" unless encounter.changed_by.blank?} #{",#{encounter.date_changed.strftime('%Y-%m-%d %H:%M:%S')}" unless encounter.date_changed.blank?})
|
596
|
+
SQL
|
597
|
+
end
|
598
|
+
row_id = ActiveRecord::Base.connection.select_one('SELECT LAST_INSERT_ID() AS id')['id']
|
599
|
+
common_encounter_void(encounter, primary_patient, row_id)
|
600
|
+
row_id
|
601
|
+
end
|
602
|
+
|
603
|
+
def common_encounter_void(encounter, primary_patient, new_encounter_id)
|
604
|
+
encounter.update(void_reason: "Merged into patient ##{primary_patient.patient_id}:#{new_encounter_id}",
|
605
|
+
voided: 1, date_voided: Time.now, voided_by: User.current.id)
|
606
|
+
end
|
607
|
+
|
608
|
+
# method to process encounter obs
|
609
|
+
def process_encounter_obs(encounter, primary_patient, secondary_patient, encounter_map)
|
610
|
+
records = encounter.observations.where(concept_id: ConceptName.find_by(name: 'Refer to ART clinician').concept_id)
|
611
|
+
return if records.blank?
|
612
|
+
|
613
|
+
records.each do |obs|
|
614
|
+
primary_obs = Observation.find_by("person_id = #{primary_patient.id} AND concept_id = #{obs.concept_id} AND
|
615
|
+
DATE(obs_datetime) = DATE('#{obs.obs_datetime.strftime('%Y-%m-%d')}')")
|
616
|
+
next if primary_obs.blank?
|
617
|
+
|
618
|
+
# we are trying to handle the scenario where the primary had also referred this patient to clinician
|
619
|
+
# then we shouldn't do anything. If secondary was referred to a clinician and primary was not then merge
|
620
|
+
unless primary_obs.value_coded != obs.value_coded && primary_obs.value_coded == ConceptName.find_by(name: 'No')
|
621
|
+
next
|
622
|
+
end
|
623
|
+
|
624
|
+
result = process_obervation_merging(obs, primary_patient, encounter_map, secondary_patient)
|
625
|
+
@obs_map[obs.id] = result.id if result
|
626
|
+
# one needs to voide the primary
|
627
|
+
primary_obs.update(void_reason: "Merged into patient ##{primary_patient.id}:#{result.id}", voided: 1,
|
628
|
+
date_voided: Time.now, voided_by: User.current.id)
|
629
|
+
# now one needs to added all obs that occured after this choice of referral
|
630
|
+
# these will be by the clinician/specialist
|
631
|
+
Observation.where('encounter_id = ? AND obs_datetime >= ? AND obs_datetime <= ? person_id = ? ', encounter.id,
|
632
|
+
obs.obs_datetime, obs.obs_datetime.end_of_day, secondary_patient.id).each do |observation|
|
633
|
+
if check_clinician?(observation.creator)
|
634
|
+
result = process_obervation_merging(observation, primary_patient, encounter_map, secondary_patient)
|
635
|
+
@obs_map[obs.id] = result.id if result
|
636
|
+
end
|
637
|
+
end
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
def reassign_remote_patient_npid(patient_doc_id)
|
642
|
+
response, status = dde_client.post('reassign_npid', { doc_id: patient_doc_id })
|
643
|
+
|
644
|
+
raise "Failed to reassign remote patient npid: Dde Response => #{status} - #{response}" unless status == 200
|
645
|
+
|
646
|
+
response
|
647
|
+
end
|
648
|
+
|
649
|
+
def find_remote_patient_npid(remote_patient)
|
650
|
+
npid = remote_patient['npid']
|
651
|
+
return npid unless npid.blank?
|
652
|
+
|
653
|
+
remote_patient['identifiers'].each do |identifier|
|
654
|
+
# NOTE: Dde returns identifiers as either a list of maps of
|
655
|
+
# identifier_type => identifier or simply a map of
|
656
|
+
# identifier_type => identifier. In the latter case the NPID is
|
657
|
+
# not included in the identifiers object hence returning nil.
|
658
|
+
return nil if identifier.instance_of?(Array)
|
659
|
+
|
660
|
+
npid = identifier['National patient identifier']
|
661
|
+
return npid unless npid.blank?
|
662
|
+
end
|
663
|
+
|
664
|
+
nil
|
665
|
+
end
|
666
|
+
|
667
|
+
# Convert a Dde person to an openmrs person.
|
668
|
+
#
|
669
|
+
# NOTE: This creates a person on the database.
|
670
|
+
def save_remote_patient(remote_patient)
|
671
|
+
LOGGER.debug "Converting Dde person to openmrs: #{remote_patient}"
|
672
|
+
|
673
|
+
person = person_service.create_person(
|
674
|
+
birthdate: remote_patient['birthdate'],
|
675
|
+
birthdate_estimated: remote_patient['birthdate_estimated'],
|
676
|
+
gender: remote_patient['gender']
|
677
|
+
)
|
678
|
+
|
679
|
+
person_service.create_person_name(
|
680
|
+
person, given_name: remote_patient['given_name'],
|
681
|
+
family_name: remote_patient['family_name'],
|
682
|
+
middle_name: remote_patient['middle_name']
|
683
|
+
)
|
684
|
+
|
685
|
+
remote_patient_attributes = remote_patient['attributes']
|
686
|
+
person_service.create_person_address(
|
687
|
+
person, home_village: remote_patient_attributes['home_village'],
|
688
|
+
home_traditional_authority: remote_patient_attributes['home_traditional_authority'],
|
689
|
+
home_district: remote_patient_attributes['home_district'],
|
690
|
+
current_village: remote_patient_attributes['current_village'],
|
691
|
+
current_traditional_authority: remote_patient_attributes['current_traditional_authority'],
|
692
|
+
current_district: remote_patient_attributes['current_district']
|
693
|
+
)
|
694
|
+
|
695
|
+
person_service.create_person_attributes(
|
696
|
+
person, cell_phone_number: remote_patient_attributes['cellphone_number'],
|
697
|
+
occupation: remote_patient_attributes['occupation']
|
698
|
+
)
|
699
|
+
|
700
|
+
patient = Patient.create(patient_id: person.id)
|
701
|
+
merging_service.link_local_to_remote_patient(patient, remote_patient)
|
702
|
+
end
|
703
|
+
|
704
|
+
def patient_service
|
705
|
+
PatientService.new
|
706
|
+
end
|
707
|
+
|
708
|
+
def dde_enabled?
|
709
|
+
return @dde_enabled unless @dde_enabled.nil?
|
710
|
+
|
711
|
+
property = GlobalProperty.find_by_property('dde_enabled')
|
712
|
+
return @dde_enabled = false unless property&.property_value
|
713
|
+
|
714
|
+
@dde_enabled = case property.property_value
|
715
|
+
when /true/i then true
|
716
|
+
when /false/i then false
|
717
|
+
else raise "Invalid value for property dde_enabled: #{property.property_value}"
|
718
|
+
end
|
719
|
+
end
|
720
|
+
|
721
|
+
def dde_client
|
722
|
+
if @dde_client.respond_to?(:call)
|
723
|
+
# HACK: Allows the dde_client to be passed in as a callable to be passed
|
724
|
+
# in as a callable to enable lazy instantiation. The dde_client is
|
725
|
+
# not required for local merges (thus no need to instantiate it).
|
726
|
+
@dde_client.call
|
727
|
+
else
|
728
|
+
@dde_client
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
732
|
+
def female_male_merge?(primary, secondary)
|
733
|
+
primary.gender != secondary.gender
|
734
|
+
end
|
735
|
+
|
736
|
+
def secondary_female?(secondary)
|
737
|
+
secondary.gender.match(/f/i)
|
738
|
+
end
|
739
|
+
|
740
|
+
def void_visit_type_encounter(primary, secondary, name)
|
741
|
+
Encounter.where(patient_id: secondary.patient_id,
|
742
|
+
visit_type: VisitType.find_by_name(name)).each do |encounter|
|
743
|
+
encounter.void("Merged into patient ##{primary.patient_id}:0")
|
744
|
+
end
|
745
|
+
end
|
746
|
+
|
747
|
+
def female_concepts
|
748
|
+
concept_ids = []
|
749
|
+
concept_ids << concept('BREASTFEEDING').concept_id
|
750
|
+
concept_ids << concept('BREAST FEEDING').concept_id
|
751
|
+
concept_ids << concept('PATIENT PREGNANT').concept_id
|
752
|
+
concept_ids << concept('Family planning method').concept_id
|
753
|
+
concept_ids << concept('Is patient pregnant?').concept_id
|
754
|
+
concept_ids << concept('Is patient breast feeding?').concept_id
|
755
|
+
concept_ids << concept('Patient using family planning').concept_id
|
756
|
+
concept_ids << concept('Method of family planning').concept_id
|
757
|
+
concept_ids << concept('Offer CxCa').concept_id
|
758
|
+
concept_ids << concept('Family planning, action to take').concept_id
|
759
|
+
concept_ids << concept('Why does the woman not use birth control').concept_id
|
760
|
+
concept_ids << concept('CxCa test date').concept_id
|
761
|
+
concept_ids << concept('Reason for NOT offering CxCa').concept_id
|
762
|
+
concept_ids
|
763
|
+
end
|
764
|
+
|
765
|
+
def female_obs?(obs)
|
766
|
+
concepts = female_concepts
|
767
|
+
concepts.include?(obs.concept_id) || concepts.include?(obs.value_coded)
|
768
|
+
end
|
769
|
+
end
|