mahis-dde 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 Dde::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
+ Dde::MergeAuditService.new.create_merge_audit(primary_patient.id, secondary_patient.id, merge_type) if defined?(Dde::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