dde_client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|