mahis-dde 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +20 -0
- data/Rakefile +8 -0
- data/app/assets/config/dde_manifest.js +1 -0
- data/app/assets/stylesheets/dde/application.css +15 -0
- data/app/controllers/dde/api/v1/dde_controller.rb +92 -0
- data/app/controllers/dde/api/v1/rollback_controller.rb +25 -0
- data/app/controllers/dde/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/dde_client.rb +162 -0
- data/app/services/dde/dde_service.rb +643 -0
- data/app/services/dde/matcher.rb +92 -0
- data/app/services/dde/merging_service.rb +769 -0
- data/app/services/dde/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_error.rb +3 -0
- data/lib/dde/engine.rb +5 -0
- data/lib/dde/version.rb +3 -0
- data/lib/dde.rb +6 -0
- data/lib/tasks/dde_tasks.rake +4 -0
- metadata +108 -0
@@ -0,0 +1,643 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Dde::DdeService
|
4
|
+
require_relative './matcher'
|
5
|
+
|
6
|
+
class DdeError < StandardError; end
|
7
|
+
|
8
|
+
Dde_CONFIG_PATH = Rails.root.join('config', 'dde.yml')
|
9
|
+
LOGGER = Rails.logger
|
10
|
+
|
11
|
+
# Limit all find queries for local patients to this
|
12
|
+
PATIENT_SEARCH_RESULTS_LIMIT = 10
|
13
|
+
|
14
|
+
attr_accessor :visit_type
|
15
|
+
|
16
|
+
include ModelUtils
|
17
|
+
|
18
|
+
def initialize(visit_type:)
|
19
|
+
raise InvalidParameterError, 'VisitType (visit_type_id) is required' unless visit_type
|
20
|
+
|
21
|
+
@visit_type = visit_type
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.dde_enabled?
|
25
|
+
property = GlobalProperty.find_by_property('dde_enabled')&.property_value
|
26
|
+
return false unless property
|
27
|
+
|
28
|
+
case property
|
29
|
+
when /true/i then true
|
30
|
+
when /false/i then false
|
31
|
+
else raise "Invalid value for property dde_enabled: #{property.property_value}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_connection
|
36
|
+
response = { connection_available: false, message: 'No connection to Dde', status: 500 }
|
37
|
+
begin
|
38
|
+
result, status = dde_client
|
39
|
+
response[:connection_available] = status == 200
|
40
|
+
response[:message] = result
|
41
|
+
rescue StandardError => exception
|
42
|
+
LOGGER.error "Failed to connect to Dde: #{exception.message}"
|
43
|
+
response[:message] = exception.message
|
44
|
+
end
|
45
|
+
response
|
46
|
+
end
|
47
|
+
|
48
|
+
# Registers local OpenMRS patient in Dde
|
49
|
+
#
|
50
|
+
# On success patient get two identifiers under the types
|
51
|
+
# 'Dde person document ID' and 'National id'. The
|
52
|
+
# 'Dde person document ID' is the patient's record ID in the local
|
53
|
+
# Dde instance and the 'National ID' is the national unique identifier
|
54
|
+
# for the patient.
|
55
|
+
def create_patient(patient)
|
56
|
+
push_local_patient_to_dde(patient)
|
57
|
+
end
|
58
|
+
|
59
|
+
def remaining_npids
|
60
|
+
response, status = dde_client.get("/location_npid_status?location_id=#{Location.current_location.id}")
|
61
|
+
raise DdeError, "Failed to fetch remaining npids: #{status} - #{response}" unless status == 200
|
62
|
+
|
63
|
+
response
|
64
|
+
end
|
65
|
+
|
66
|
+
def void_patient(patient, reason)
|
67
|
+
raise ArgumentError, "Can't request a Dde void for a non-voided patient" unless patient.voided?
|
68
|
+
raise ArgumentError, 'void_reason is required' if reason.blank?
|
69
|
+
|
70
|
+
doc_id = PatientIdentifier.unscoped
|
71
|
+
.where(identifier_type: dde_doc_id_type, patient: patient)
|
72
|
+
.order(:date_voided)
|
73
|
+
.last
|
74
|
+
&.identifier
|
75
|
+
return patient unless doc_id
|
76
|
+
|
77
|
+
response, status = dde_client.delete("void_person/#{doc_id}?void_reason=#{reason}")
|
78
|
+
raise DdeError, "Failed to void person in Dde: #{status} - #{response}" unless status == 200
|
79
|
+
|
80
|
+
patient
|
81
|
+
end
|
82
|
+
|
83
|
+
# Updates patient demographics in Dde.
|
84
|
+
#
|
85
|
+
# Local patient is not affected in anyway by the update
|
86
|
+
def update_patient(patient)
|
87
|
+
dde_patient = openmrs_to_dde_patient(patient)
|
88
|
+
response, status = dde_client.post('update_person', dde_patient)
|
89
|
+
|
90
|
+
raise DdeError, "Failed to update person in Dde: #{response}" unless status == 200
|
91
|
+
|
92
|
+
patient
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Pushes a footprint for patient in current visit_type to Dde
|
97
|
+
def create_patient_footprint(patient, date = nil, creator_id = nil)
|
98
|
+
LOGGER.debug("Pushing footprint to Dde for patient ##{patient.patient_id}")
|
99
|
+
doc_id = find_patient_doc_id(patient)
|
100
|
+
unless doc_id
|
101
|
+
LOGGER.debug("Patient ##{patient.patient_id} is not a Dde patient")
|
102
|
+
return
|
103
|
+
end
|
104
|
+
|
105
|
+
response, status = dde_client.post('update_footprint', person_uuid: doc_id,
|
106
|
+
location_id: Location.current_location_health_center.location_id,
|
107
|
+
visit_type_id: visit_type.id,
|
108
|
+
encounter_datetime: date || Date.tody,
|
109
|
+
user_id: creator_id || User.current.user_id)
|
110
|
+
|
111
|
+
LOGGER.warn("Failed to push patient footprint to Dde: #{status} - #{response}") unless status == 200
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Updates local patient with demographics currently in Dde.
|
116
|
+
def update_local_patient(patient, update_npid: false)
|
117
|
+
doc_id = patient_doc_id(patient)
|
118
|
+
unless doc_id
|
119
|
+
Rails.logger.warn("No Dde doc_id found for patient ##{patient.patient_id}")
|
120
|
+
push_local_patient_to_dde(patient)
|
121
|
+
return patient
|
122
|
+
end
|
123
|
+
|
124
|
+
dde_patient = find_remote_patients_by_doc_id(doc_id).first
|
125
|
+
unless dde_patient
|
126
|
+
Rails.logger.warn("Couldn't find patient ##{patient.patient_id} in Dde by doc_id ##{doc_id}")
|
127
|
+
push_local_patient_to_dde(patient)
|
128
|
+
return patient
|
129
|
+
end
|
130
|
+
|
131
|
+
if update_npid
|
132
|
+
merging_service.link_local_to_remote_patient(patient, dde_patient)
|
133
|
+
return patient
|
134
|
+
end
|
135
|
+
|
136
|
+
person_service.update_person(patient.person, dde_patient_to_local_person(dde_patient))
|
137
|
+
patient
|
138
|
+
end
|
139
|
+
|
140
|
+
# Import patients from Dde using doc id
|
141
|
+
def import_patients_by_doc_id(doc_id)
|
142
|
+
doc_id_type = patient_identifier_type('Dde person document id')
|
143
|
+
locals = patient_service.find_patients_by_identifier(doc_id, doc_id_type).limit(PATIENT_SEARCH_RESULTS_LIMIT)
|
144
|
+
remotes = find_remote_patients_by_doc_id(doc_id)
|
145
|
+
|
146
|
+
import_remote_patient(locals, remotes)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Imports patients from Dde to the local database
|
150
|
+
def import_patients_by_npid(npid)
|
151
|
+
doc_id_type = patient_identifier_type('National id')
|
152
|
+
locals = patient_service.find_patients_by_identifier(npid, doc_id_type).limit(PATIENT_SEARCH_RESULTS_LIMIT)
|
153
|
+
remotes = find_remote_patients_by_npid(npid)
|
154
|
+
|
155
|
+
import_remote_patient(locals, remotes)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Similar to import_patients_by_npid but uses name and gender instead of npid
|
159
|
+
def import_patients_by_name_and_gender(given_name, family_name, gender)
|
160
|
+
locals = patient_service.find_patients_by_name_and_gender(given_name, family_name, gender).limit(PATIENT_SEARCH_RESULTS_LIMIT)
|
161
|
+
|
162
|
+
remotes = begin
|
163
|
+
find_remote_patients_by_name_and_gender(given_name, family_name, gender)
|
164
|
+
rescue StandardError
|
165
|
+
[]
|
166
|
+
end
|
167
|
+
|
168
|
+
import_remote_patient(locals, remotes)
|
169
|
+
end
|
170
|
+
|
171
|
+
def find_patients_by_npid(npid)
|
172
|
+
locals = patient_service.find_patients_by_npid(npid).limit(PATIENT_SEARCH_RESULTS_LIMIT)
|
173
|
+
remotes = find_remote_patients_by_npid(npid)
|
174
|
+
|
175
|
+
package_patients(locals, remotes, auto_push_singular_local: true)
|
176
|
+
end
|
177
|
+
|
178
|
+
def find_patients_by_name_and_gender(given_name, family_name, gender)
|
179
|
+
locals = []
|
180
|
+
begin
|
181
|
+
locals = patient_service.find_patients_by_name_and_gender(given_name, family_name, gender).limit(PATIENT_SEARCH_RESULTS_LIMIT)
|
182
|
+
remotes = find_remote_patients_by_name_and_gender(given_name, family_name, gender)
|
183
|
+
|
184
|
+
package_patients(locals, remotes)
|
185
|
+
rescue StandardError => e
|
186
|
+
Rails.logger.warn("Error packaging patients: #{e.message}")
|
187
|
+
package_patients(locals, [])
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def find_patient_updates(local_patient_id)
|
192
|
+
dde_doc_id_type = PatientIdentifierType.where(name: 'Dde Person Document ID')
|
193
|
+
doc_id = PatientIdentifier.find_by(patient_id: local_patient_id, identifier_type: dde_doc_id_type)
|
194
|
+
&.identifier
|
195
|
+
return nil unless doc_id
|
196
|
+
|
197
|
+
remote_patient = find_remote_patients_by_doc_id(doc_id).first
|
198
|
+
return nil unless remote_patient
|
199
|
+
|
200
|
+
Matcher.find_differences(Person.find(local_patient_id), remote_patient)
|
201
|
+
rescue DdeError => e
|
202
|
+
Rails.logger.warn("Check for Dde patient updates failed: #{e.message}")
|
203
|
+
nil
|
204
|
+
end
|
205
|
+
|
206
|
+
# Matches patients using a bunch of demographics
|
207
|
+
def match_patients_by_demographics(family_name:, given_name:, birthdate:,
|
208
|
+
gender:, home_district:, home_traditional_authority:,
|
209
|
+
home_village:, birthdate_estimated: 0)
|
210
|
+
response, status = dde_client.post(
|
211
|
+
'search/people', family_name: family_name,
|
212
|
+
given_name: given_name,
|
213
|
+
gender: gender,
|
214
|
+
birthdate: birthdate,
|
215
|
+
birthdate_estimated: !birthdate_estimated.zero?,
|
216
|
+
attributes: {
|
217
|
+
home_district: home_district,
|
218
|
+
home_traditional_authority: home_traditional_authority,
|
219
|
+
home_village: home_village
|
220
|
+
}
|
221
|
+
)
|
222
|
+
|
223
|
+
raise DdeError, "Dde patient search failed: #{status} - #{response}" unless status == 200
|
224
|
+
|
225
|
+
response.collect do |match|
|
226
|
+
doc_id = match['person']['id']
|
227
|
+
patient = patient_service.find_patients_by_identifier(
|
228
|
+
doc_id, patient_identifier_type('Dde person document id')
|
229
|
+
).first
|
230
|
+
match['person']['patient_id'] = patient&.id
|
231
|
+
match
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Trigger a merge of patients in Dde
|
236
|
+
def merge_patients(primary_patients_ids, secondary_patient_ids)
|
237
|
+
merging_service.merge_patients(primary_patients_ids, secondary_patient_ids)
|
238
|
+
end
|
239
|
+
|
240
|
+
def reassign_patient_npid(patient_ids)
|
241
|
+
patient_id = patient_ids['patient_id']
|
242
|
+
doc_id = patient_ids['doc_id']
|
243
|
+
|
244
|
+
raise InvalidParameterError, 'patient_id and/or doc_id required' if patient_id.blank? && doc_id.blank?
|
245
|
+
|
246
|
+
if doc_id.blank?
|
247
|
+
# Only have patient id thus we have patient locally only
|
248
|
+
return push_local_patient_to_dde(Patient.find(patient_ids['patient_id']))
|
249
|
+
end
|
250
|
+
|
251
|
+
# NOTE: Fail patient retrieval as early as possible before making any
|
252
|
+
# changes to Dde (ie if patient_id does not exist)
|
253
|
+
patient = patient_id.blank? ? nil : Patient.find(patient_id)
|
254
|
+
|
255
|
+
# We have a doc_id thus we can re-assign npid in Dde
|
256
|
+
# Check if person if available in Dde if not add person using doc_id
|
257
|
+
response, status = dde_client.post('search_by_doc_id', doc_id: doc_id)
|
258
|
+
if !response.blank? && status.to_i == 200
|
259
|
+
response, status = dde_client.post('reassign_npid', doc_id: doc_id)
|
260
|
+
elsif response.blank? && status.to_i == 200
|
261
|
+
return push_local_patient_to_dde(Patient.find(patient_ids['patient_id']))
|
262
|
+
end
|
263
|
+
|
264
|
+
unless status == 200 && !response.empty?
|
265
|
+
# The Dde's reassign_npid end point responds with a 200 - OK but returns
|
266
|
+
# an empty object when patient with given doc_id is not found.
|
267
|
+
raise DdeError, "Failed to reassign npid: Dde Response => #{status} - #{response}"
|
268
|
+
end
|
269
|
+
|
270
|
+
return save_remote_patient(response) unless patient
|
271
|
+
|
272
|
+
merging_service.link_local_to_remote_patient(patient, response)
|
273
|
+
end
|
274
|
+
|
275
|
+
# Convert a Dde person to an openmrs person.
|
276
|
+
#
|
277
|
+
# NOTE: This creates a person on the database.
|
278
|
+
def save_remote_patient(remote_patient)
|
279
|
+
LOGGER.debug "Converting Dde person to openmrs: #{remote_patient}"
|
280
|
+
params = dde_patient_to_local_person(remote_patient)
|
281
|
+
|
282
|
+
Person.transaction do
|
283
|
+
person = person_service.create_person(params)
|
284
|
+
|
285
|
+
patient = Patient.create(patient_id: person.id)
|
286
|
+
merging_service.link_local_to_remote_patient(patient, remote_patient)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
##
|
291
|
+
# Converts a dde_patient object into an object that can be passed to the person_service
|
292
|
+
# to create or update a person.
|
293
|
+
def dde_patient_to_local_person(dde_patient)
|
294
|
+
attributes = dde_patient.fetch('attributes')
|
295
|
+
|
296
|
+
# ActiveSupport::HashWithIndifferentAccess.new(
|
297
|
+
# birthdate: dde_patient.fetch('birthdate'),
|
298
|
+
# birthdate_estimated: dde_patient.fetch('birthdate_estimated'),
|
299
|
+
# gender: dde_patient.fetch('gender'),
|
300
|
+
# given_name: dde_patient.fetch('given_name'),
|
301
|
+
# family_name: dde_patient.fetch('family_name'),
|
302
|
+
# middle_name: dde_patient.fetch('middle_name'),
|
303
|
+
# home_village: attributes.fetch('home_village'),
|
304
|
+
# home_traditional_authority: attributes.fetch('home_traditional_authority'),
|
305
|
+
# home_district: attributes.fetch('home_district'),
|
306
|
+
# current_village: attributes.fetch('current_village'),
|
307
|
+
# current_traditional_authority: attributes.fetch('current_traditional_authority'),
|
308
|
+
# current_district: attributes.fetch('current_district')
|
309
|
+
# # cell_phone_number: attributes.fetch('cellphone_number'),
|
310
|
+
# # occupation: attributes.fetch('occupation')
|
311
|
+
# )
|
312
|
+
{
|
313
|
+
birthdate: dde_patient.fetch('birthdate'),
|
314
|
+
birthdate_estimated: dde_patient.fetch('birthdate_estimated'),
|
315
|
+
gender: dde_patient.fetch('gender'),
|
316
|
+
names: [
|
317
|
+
{
|
318
|
+
given_name: dde_patient.fetch('given_name'),
|
319
|
+
family_name: dde_patient.fetch('family_name'),
|
320
|
+
middle_name: dde_patient.fetch('middle_name')
|
321
|
+
}
|
322
|
+
],
|
323
|
+
addresses: [
|
324
|
+
{
|
325
|
+
address1: attributes.fetch('home_district'),
|
326
|
+
address3: attributes.fetch('current_district'),
|
327
|
+
county_district: attributes.fetch('home_traditional_authority'),
|
328
|
+
state_province: attributes.fetch('current_traditional_authority'),
|
329
|
+
address2: attributes.fetch('home_village'),
|
330
|
+
city_village: attributes.fetch('current_village')
|
331
|
+
}
|
332
|
+
]
|
333
|
+
}
|
334
|
+
end
|
335
|
+
|
336
|
+
private
|
337
|
+
|
338
|
+
def find_remote_patients_by_npid(npid)
|
339
|
+
response, _status = dde_client.post('search_by_npid', npid: npid)
|
340
|
+
raise DdeError, "Patient search by npid failed: Dde Response => #{response}" unless response.instance_of?(Array)
|
341
|
+
|
342
|
+
response
|
343
|
+
end
|
344
|
+
|
345
|
+
def find_remote_patients_by_name_and_gender(given_name, family_name, gender)
|
346
|
+
response = nil
|
347
|
+
begin
|
348
|
+
response, _status = dde_client.post('search_by_name_and_gender', given_name: given_name,
|
349
|
+
family_name: family_name,
|
350
|
+
gender: gender)
|
351
|
+
unless response.instance_of?(Array)
|
352
|
+
print "Patient search by name and gender failed: Dde Response => #{response}"
|
353
|
+
return []
|
354
|
+
end
|
355
|
+
rescue StandardError => e
|
356
|
+
print "Patient search by name and gender failed: Dde Response => #{e}"
|
357
|
+
return []
|
358
|
+
end
|
359
|
+
|
360
|
+
response
|
361
|
+
end
|
362
|
+
|
363
|
+
def find_remote_patients_by_doc_id(doc_id)
|
364
|
+
Rails.logger.info("Searching for Dde patient by doc_id ##{doc_id}")
|
365
|
+
response, _status = dde_client.post('search_by_doc_id', doc_id: doc_id)
|
366
|
+
raise DdeError, "Patient search by doc_id failed: Dde Response => #{response}" unless response.instance_of?(Array)
|
367
|
+
|
368
|
+
response
|
369
|
+
end
|
370
|
+
|
371
|
+
def find_patient_doc_id(patient)
|
372
|
+
patient.patient_identifiers.where(identifier_type: dde_doc_id_type).first
|
373
|
+
end
|
374
|
+
|
375
|
+
# Resolves local and remote patients and post processes the remote
|
376
|
+
# patients to take on a structure similar to that of local
|
377
|
+
# patients.
|
378
|
+
def package_patients(local_patients, remote_patients, auto_push_singular_local: false)
|
379
|
+
patients = resolve_patients(local_patients: local_patients,
|
380
|
+
remote_patients: remote_patients,
|
381
|
+
auto_push_singular_local: auto_push_singular_local)
|
382
|
+
|
383
|
+
# In some cases we may have remote patients that were previously imported but
|
384
|
+
# whose NPID has changed, we need to find and resolve these local patients.
|
385
|
+
unresolved_patients = find_patients_by_doc_id(patients[:remotes].collect { |remote_patient| remote_patient['doc_id'] })
|
386
|
+
if unresolved_patients.empty?
|
387
|
+
return { locals: patients[:locals], remotes: patients[:remotes].collect { |patient| localise_remote_patient(patient) } }
|
388
|
+
end
|
389
|
+
|
390
|
+
additional_patients = resolve_patients(local_patients: unresolved_patients, remote_patients: patients[:remotes])
|
391
|
+
|
392
|
+
{
|
393
|
+
locals: patients[:locals] + additional_patients[:locals],
|
394
|
+
remotes: additional_patients[:remotes].collect { |patient| localise_remote_patient(patient) }
|
395
|
+
}
|
396
|
+
end
|
397
|
+
|
398
|
+
# Locally saves the first unresolved remote patient.
|
399
|
+
#
|
400
|
+
# Method internally calls resolve_patients on the passed arguments then
|
401
|
+
# attempts to save the first unresolved patient in the local database.
|
402
|
+
#
|
403
|
+
# Returns: The imported patient (or nil if no local and remote patients are
|
404
|
+
# present).
|
405
|
+
def import_remote_patient(local_patients, remote_patients)
|
406
|
+
patients = resolve_patients(local_patients: local_patients, remote_patients: remote_patients)
|
407
|
+
|
408
|
+
return patients[:locals].first if patients[:remotes].empty?
|
409
|
+
|
410
|
+
save_remote_patient(patients[:remotes].first)
|
411
|
+
end
|
412
|
+
|
413
|
+
# Filters out @{param remote_patients} that exist in @{param local_patients}.
|
414
|
+
#
|
415
|
+
# Returns a hash with all resolved and unresolved remote patients:
|
416
|
+
#
|
417
|
+
# { resolved: [..,], locals: [...], remotes: [...] }
|
418
|
+
#
|
419
|
+
# NOTE: All resolved patients are available in the local database
|
420
|
+
def resolve_patients(local_patients:, remote_patients:, auto_push_singular_local: false)
|
421
|
+
remote_patients = remote_patients.dup # Will be modifying this copy
|
422
|
+
|
423
|
+
# Match all locals to remotes, popping out the matching patients from
|
424
|
+
# the list of remotes. The remaining remotes are considered unresolved
|
425
|
+
# remotes.
|
426
|
+
resolved_patients = local_patients.each_with_object([]) do |local_patient, resolved_patients|
|
427
|
+
# Local patient present on remote?
|
428
|
+
remote_patient = remote_patients.detect do |patient|
|
429
|
+
same_patient?(local_patient: local_patient, remote_patient: patient)
|
430
|
+
end
|
431
|
+
|
432
|
+
remote_patients.delete(remote_patient) if remote_patient
|
433
|
+
|
434
|
+
resolved_patients << local_patient
|
435
|
+
end
|
436
|
+
|
437
|
+
if resolved_patients.empty? && (local_patients.size.zero? && remote_patients.size == 1)
|
438
|
+
# HACK: Frontenders requested that if only a single patient exists
|
439
|
+
# remotely and locally none exists, the remote patient should be
|
440
|
+
# imported.
|
441
|
+
local_patient = find_patients_by_doc_id(remote_patients[0]['doc_id']).first
|
442
|
+
resolved_patients = [local_patient || save_remote_patient(remote_patients[0])]
|
443
|
+
remote_patients = []
|
444
|
+
elsif auto_push_singular_local && resolved_patients.size == 1\
|
445
|
+
&& remote_patients.empty? && local_only_patient?(resolved_patients.first)
|
446
|
+
# ANOTHER HACK: Push local only patient to Dde
|
447
|
+
resolved_patients = [push_local_patient_to_dde(resolved_patients[0])]
|
448
|
+
else
|
449
|
+
resolved_patients = local_patients
|
450
|
+
end
|
451
|
+
|
452
|
+
{ locals: resolved_patients, remotes: remote_patients }
|
453
|
+
end
|
454
|
+
|
455
|
+
# Checks if patient only exists on local database
|
456
|
+
def local_only_patient?(patient)
|
457
|
+
!(patient.patient_identifiers.where(identifier_type: patient_identifier_type('National id')).exists?\
|
458
|
+
&& patient.patient_identifiers.where(identifier_type: patient_identifier_type('Dde person document id')).exists?)
|
459
|
+
end
|
460
|
+
|
461
|
+
# Matches local and remote patient
|
462
|
+
def same_patient?(local_patient:, remote_patient:)
|
463
|
+
PatientIdentifier.where(patient: local_patient, identifier_type: dde_doc_id_type).any? do |doc_id|
|
464
|
+
doc_id.identifier == remote_patient['doc_id']
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
# Saves local patient to Dde and links the two using the IDs
|
469
|
+
# generated by Dde.
|
470
|
+
def push_local_patient_to_dde(patient)
|
471
|
+
Rails.logger.info("Pushing local patient ##{patient.patient_id} to Dde")
|
472
|
+
response, status = dde_client.post('add_person', openmrs_to_dde_patient(patient))
|
473
|
+
|
474
|
+
if status == 422
|
475
|
+
error = UnprocessableEntityError.new("Failed to create patient in Dde: #{response.to_json}")
|
476
|
+
error.add_entity(patient)
|
477
|
+
raise error
|
478
|
+
end
|
479
|
+
|
480
|
+
raise response.to_json if status != 200
|
481
|
+
|
482
|
+
merging_service.link_local_to_remote_patient(patient, response)
|
483
|
+
end
|
484
|
+
|
485
|
+
# Converts a remote patient coming from Dde into a structure similar
|
486
|
+
# to that of a local patient
|
487
|
+
def localise_remote_patient(patient)
|
488
|
+
Patient.new(
|
489
|
+
patient_identifiers: localise_remote_patient_identifiers(patient),
|
490
|
+
person: Person.new(
|
491
|
+
names: localise_remote_patient_names(patient),
|
492
|
+
addresses: localise_remote_patient_addresses(patient),
|
493
|
+
birthdate: patient['birthdate'],
|
494
|
+
birthdate_estimated: patient['birthdate_estimated'],
|
495
|
+
gender: patient['gender']
|
496
|
+
)
|
497
|
+
)
|
498
|
+
end
|
499
|
+
|
500
|
+
def localise_remote_patient_identifiers(remote_patient)
|
501
|
+
[PatientIdentifier.new(identifier: remote_patient['npid'],
|
502
|
+
identifier_type: patient_identifier_type('National ID')),
|
503
|
+
PatientIdentifier.new(identifier: remote_patient['doc_id'],
|
504
|
+
identifier_type: patient_identifier_type('Dde Person Document ID'))]
|
505
|
+
end
|
506
|
+
|
507
|
+
def localise_remote_patient_names(remote_patient)
|
508
|
+
[PersonName.new(given_name: remote_patient['given_name'],
|
509
|
+
family_name: remote_patient['family_name'],
|
510
|
+
middle_name: remote_patient['middle_name'])]
|
511
|
+
end
|
512
|
+
|
513
|
+
def localise_remote_patient_addresses(remote_patient)
|
514
|
+
address = PersonAddress.new
|
515
|
+
address.city_village = remote_patient['attributes']['home_village']
|
516
|
+
address.home_traditional_authority = remote_patient['attributes']['home_traditional_authority']
|
517
|
+
address.home_district = remote_patient['attributes']['home_district']
|
518
|
+
address.current_village = remote_patient['attributes']['current_village']
|
519
|
+
address.current_traditional_authority = remote_patient['attributes']['current_traditional_authority']
|
520
|
+
address.current_district = remote_patient['attributes']['current_district']
|
521
|
+
[address]
|
522
|
+
end
|
523
|
+
|
524
|
+
def dde_client
|
525
|
+
client = Dde::DdeClient.new
|
526
|
+
|
527
|
+
connection = dde_connections[visit_type.id]
|
528
|
+
|
529
|
+
@dde_connections[visit_type.id] = if connection && %i[url username password].all? { |key| connection&.key?(key) }
|
530
|
+
client.restore_connection(connection)
|
531
|
+
else
|
532
|
+
client.connect(url: dde_config[:url],
|
533
|
+
username: dde_config[:username],
|
534
|
+
password: dde_config[:password])
|
535
|
+
end
|
536
|
+
|
537
|
+
client
|
538
|
+
end
|
539
|
+
|
540
|
+
# Loads a dde client into the dde_clients_cache for the
|
541
|
+
def dde_config
|
542
|
+
main_config = YAML.load_file(Dde_CONFIG_PATH)['config']
|
543
|
+
raise 'No configuration for Dde found' unless main_config
|
544
|
+
|
545
|
+
visit_type_config = main_config[visit_type.name.downcase]
|
546
|
+
raise "No Dde config for visit_type #{visit_type.name.downcase} found" unless visit_type_config
|
547
|
+
|
548
|
+
{
|
549
|
+
url: main_config['url'],
|
550
|
+
username: visit_type_config['username'],
|
551
|
+
password: visit_type_config['password']
|
552
|
+
}
|
553
|
+
end
|
554
|
+
|
555
|
+
# Converts an openmrs patient structure to a Dde person structure
|
556
|
+
def openmrs_to_dde_patient(patient)
|
557
|
+
LOGGER.debug "Converting OpenMRS person to dde_patient: #{patient}"
|
558
|
+
person = patient.person
|
559
|
+
|
560
|
+
person_name = person.names[0]
|
561
|
+
person_address = person.addresses[0]
|
562
|
+
person_attributes = filter_person_attributes(person.person_attributes)
|
563
|
+
|
564
|
+
dde_patient = HashWithIndifferentAccess.new(
|
565
|
+
given_name: person_name.given_name,
|
566
|
+
family_name: person_name.family_name,
|
567
|
+
gender: person.gender&.first,
|
568
|
+
birthdate: person.birthdate,
|
569
|
+
birthdate_estimated: person.birthdate_estimated ? 1 : 0,
|
570
|
+
attributes: {
|
571
|
+
current_district: person_address ? person_address.address3 : nil,
|
572
|
+
current_traditional_authority: person_address ? person_address.state_province : nil,
|
573
|
+
current_village: person_address ? person_address.city_village : nil,
|
574
|
+
home_district: person_address ? person_address.address1 : nil,
|
575
|
+
home_village: person_address ? person_address.city_village : nil,
|
576
|
+
home_traditional_authority: person_address ? person_address.county_district : nil,
|
577
|
+
occupation: person_attributes ? person_attributes[:occupation] : nil
|
578
|
+
}
|
579
|
+
)
|
580
|
+
|
581
|
+
doc_id = patient.patient_identifiers.where(identifier_type: patient_identifier_type('Dde person document id')).first
|
582
|
+
dde_patient[:doc_id] = doc_id.identifier if doc_id
|
583
|
+
|
584
|
+
LOGGER.debug "Converted openmrs person to dde_patient: #{dde_patient}"
|
585
|
+
dde_patient
|
586
|
+
end
|
587
|
+
|
588
|
+
def filter_person_attributes(person_attributes)
|
589
|
+
return nil unless person_attributes
|
590
|
+
|
591
|
+
person_attributes.each_with_object({}) do |attr, filtered|
|
592
|
+
case attr.person_attribute_type.name.downcase.gsub(/\s+/, '_')
|
593
|
+
when 'cell_phone_number'
|
594
|
+
filtered[:cell_phone_number] = attr.value
|
595
|
+
when 'occupation'
|
596
|
+
filtered[:occupation] = attr.value
|
597
|
+
when 'birthplace'
|
598
|
+
filtered[:home_district] = attr.value
|
599
|
+
when 'home_village'
|
600
|
+
filtered[:home_village] = attr.value
|
601
|
+
when 'ancestral_traditional_authority'
|
602
|
+
filtered[:home_traditional_authority] = attr.value
|
603
|
+
end
|
604
|
+
end
|
605
|
+
end
|
606
|
+
|
607
|
+
def patient_doc_id(patient)
|
608
|
+
PatientIdentifier
|
609
|
+
.joins(:identifier_type)
|
610
|
+
.merge(PatientIdentifierType.where(name: 'Dde person document id'))
|
611
|
+
.where(patient: patient)
|
612
|
+
.first
|
613
|
+
&.identifier
|
614
|
+
end
|
615
|
+
|
616
|
+
def dde_doc_id_type
|
617
|
+
PatientIdentifierType.find_by_name('Dde Person document ID')
|
618
|
+
end
|
619
|
+
|
620
|
+
def find_patients_by_doc_id(doc_ids)
|
621
|
+
identifiers = PatientIdentifier.joins(:identifier_type)
|
622
|
+
.merge(PatientIdentifierType.where(name: 'Dde Person Document ID'))
|
623
|
+
.where(identifier: doc_ids)
|
624
|
+
Patient.joins(:identifiers).merge(identifiers).distinct
|
625
|
+
end
|
626
|
+
|
627
|
+
def person_service
|
628
|
+
PersonService.new
|
629
|
+
end
|
630
|
+
|
631
|
+
def patient_service
|
632
|
+
PatientService.new
|
633
|
+
end
|
634
|
+
|
635
|
+
def merging_service
|
636
|
+
Dde::MergingService.new(self, -> { dde_client })
|
637
|
+
end
|
638
|
+
|
639
|
+
# A cache for all connections to dde (indexed by visit_type id)
|
640
|
+
def dde_connections
|
641
|
+
@dde_connections ||= {}
|
642
|
+
end
|
643
|
+
end
|