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.
@@ -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