dde_mahis 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_mahis/api/v1/dde_controller.rb +94 -0
- data/app/controllers/dde_mahis/api/v1/rollback_controller.rb +32 -0
- data/app/controllers/dde_mahis/application_controller.rb +4 -0
- data/app/helpers/dde_mahis/application_helper.rb +4 -0
- data/app/jobs/dde_mahis/application_job.rb +4 -0
- data/app/mailers/dde_mahis/application_mailer.rb +6 -0
- data/app/models/dde_mahis/application_record.rb +5 -0
- data/app/services/dde_client.rb +162 -0
- data/app/services/dde_service.rb +643 -0
- data/app/services/matcher.rb +92 -0
- data/app/services/merge_audit_service.rb +56 -0
- data/app/services/merging_service.rb +769 -0
- data/app/services/rollback_service.rb +320 -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_mahis/client_error.rb +3 -0
- data/lib/dde_mahis/engine.rb +7 -0
- data/lib/dde_mahis/version.rb +3 -0
- data/lib/dde_mahis.rb +6 -0
- data/lib/tasks/dde_tasks.rake +4 -0
- metadata +115 -0
@@ -0,0 +1,643 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DdeService
|
4
|
+
require_relative './matcher'
|
5
|
+
|
6
|
+
class DdeMahisError < StandardError; end
|
7
|
+
|
8
|
+
DdeMahis_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 DdeMahis', 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 DdeMahis: #{exception.message}"
|
43
|
+
response[:message] = exception.message
|
44
|
+
end
|
45
|
+
response
|
46
|
+
end
|
47
|
+
|
48
|
+
# Registers local OpenMRS patient in DdeMahis
|
49
|
+
#
|
50
|
+
# On success patient get two identifiers under the types
|
51
|
+
# 'DdeMahis person document ID' and 'National id'. The
|
52
|
+
# 'DdeMahis person document ID' is the patient's record ID in the local
|
53
|
+
# DdeMahis 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 DdeMahisError, "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 DdeMahis 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 DdeMahisError, "Failed to void person in DdeMahis: #{status} - #{response}" unless status == 200
|
79
|
+
|
80
|
+
patient
|
81
|
+
end
|
82
|
+
|
83
|
+
# Updates patient demographics in DdeMahis.
|
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 DdeMahisError, "Failed to update person in DdeMahis: #{response}" unless status == 200
|
91
|
+
|
92
|
+
patient
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Pushes a footprint for patient in current visit_type to DdeMahis
|
97
|
+
def create_patient_footprint(patient, date = nil, creator_id = nil)
|
98
|
+
LOGGER.debug("Pushing footprint to DdeMahis 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 DdeMahis 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 DdeMahis: #{status} - #{response}") unless status == 200
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Updates local patient with demographics currently in DdeMahis.
|
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 DdeMahis 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 DdeMahis 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 DdeMahis using doc id
|
141
|
+
def import_patients_by_doc_id(doc_id)
|
142
|
+
doc_id_type = patient_identifier_type('DdeMahis 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 DdeMahis 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: 'DdeMahis 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 DdeMahisError => e
|
202
|
+
Rails.logger.warn("Check for DdeMahis 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 DdeMahisError, "DdeMahis 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('DdeMahis 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 DdeMahis
|
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 DdeMahis (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 DdeMahis
|
256
|
+
# Check if person if available in DdeMahis 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 DdeMahis'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 DdeMahisError, "Failed to reassign npid: DdeMahis 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 DdeMahis 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 DdeMahis 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 DdeMahisError, "Patient search by npid failed: DdeMahis 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: DdeMahis Response => #{response}"
|
353
|
+
return []
|
354
|
+
end
|
355
|
+
rescue StandardError => e
|
356
|
+
print "Patient search by name and gender failed: DdeMahis 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 DdeMahis patient by doc_id ##{doc_id}")
|
365
|
+
response, _status = dde_client.post('search_by_doc_id', doc_id: doc_id)
|
366
|
+
raise DdeMahisError, "Patient search by doc_id failed: DdeMahis 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 DdeMahis
|
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('DdeMahis 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 DdeMahis and links the two using the IDs
|
469
|
+
# generated by DdeMahis.
|
470
|
+
def push_local_patient_to_dde(patient)
|
471
|
+
Rails.logger.info("Pushing local patient ##{patient.patient_id} to DdeMahis")
|
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 DdeMahis: #{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 DdeMahis 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('DdeMahis 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 = 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(DdeMahis_CONFIG_PATH)['config']
|
543
|
+
raise 'No configuration for DdeMahis found' unless main_config
|
544
|
+
|
545
|
+
visit_type_config = main_config[visit_type.name.downcase]
|
546
|
+
raise "No DdeMahis 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 DdeMahis 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('DdeMahis 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: 'DdeMahis 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('DdeMahis 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: 'DdeMahis 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
|
+
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
|