dde_client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +37 -0
- data/Rakefile +12 -0
- data/app/assets/config/dde_client_manifest.js +1 -0
- data/app/assets/stylesheets/dde_client/application.css +15 -0
- data/app/controllers/dde_client/api/v1/dde_controller.rb +92 -0
- data/app/controllers/dde_client/api/v1/rollback_controller.rb +25 -0
- data/app/controllers/dde_client/application_controller.rb +4 -0
- data/app/helpers/dde/application_helper.rb +4 -0
- data/app/jobs/dde/application_job.rb +4 -0
- data/app/mailers/dde/application_mailer.rb +6 -0
- data/app/models/dde/application_record.rb +5 -0
- data/app/services/dde_client/dde_client.rb +162 -0
- data/app/services/dde_client/dde_service.rb +643 -0
- data/app/services/dde_client/matcher.rb +92 -0
- data/app/services/dde_client/merging_service.rb +769 -0
- data/app/services/dde_client/rollback_service.rb +320 -0
- data/app/services/merge_audit_service.rb +56 -0
- data/app/utils/model_utils.rb +62 -0
- data/app/views/layouts/dde/application.html.erb +15 -0
- data/config/routes.rb +14 -0
- data/lib/dde_client/client_error.rb +3 -0
- data/lib/dde_client/engine.rb +5 -0
- data/lib/dde_client/version.rb +5 -0
- data/lib/dde_client.rb +9 -0
- metadata +100 -0
@@ -0,0 +1,643 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DdeClient::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 = DdeClient::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
|
+
DdeClient::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
|