super_ehr 1.0.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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/super_ehr.rb +518 -0
  3. metadata +45 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: adddf5fefce11f3bcd1124454e4fac6ece271e04
4
+ data.tar.gz: 552b22b0b6de40350469182a3337e9385c314f30
5
+ SHA512:
6
+ metadata.gz: 58aec116ab306abb82d69cd8b1be56e974f582899efe87deb299bac4058e0d5261e2b3201219722e31dd53d3bf5c4a8ad533ebd9286812c04e27c6880ff4f2da
7
+ data.tar.gz: e601918bc9195f1c1656cef29f613ea2e35d6ec4765fe09b49468c28f0301e4a3fdd1b2f128e1f6dcdb33475a74a403b15ec9b244e15f61d6a43af71065d54f3
data/lib/super_ehr.rb ADDED
@@ -0,0 +1,518 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'httparty'
4
+ require 'httmultiparty'
5
+ require 'builder'
6
+ require 'time'
7
+
8
+ module SuperEHR
9
+
10
+ def self.not_implemented(func)
11
+ puts "ERROR: #{func} not implemented"
12
+ raise NotImplementedError
13
+ end
14
+
15
+ class BaseEHR
16
+
17
+ ### API SPECIFIC HOUSEKEEPING ###
18
+
19
+ # initialize necessary components
20
+ def initialize(default_params={})
21
+ @default_params = default_params
22
+ end
23
+
24
+ def get_default_params
25
+ return {}
26
+ end
27
+
28
+ def get_request_headers
29
+ return {}
30
+ end
31
+
32
+ def get_request_body
33
+ return {}
34
+ end
35
+
36
+ def get_request_url(endpoint)
37
+ return "/#{endpoint}"
38
+ end
39
+
40
+ def refresh_token
41
+ SuperEHR.not_implemented(__callee__)
42
+ end
43
+
44
+ # make a HTTP request
45
+ def make_request(request_type, endpoint, request_params={}, use_default_params=true,
46
+ to_json=true)
47
+
48
+ if use_default_params
49
+ params = get_default_params
50
+ params = params.merge(request_params)
51
+ else
52
+ params = request_params
53
+ end
54
+
55
+ headers = get_request_headers
56
+ url = get_request_url(endpoint)
57
+
58
+ if request_type == "GET"
59
+ response = HTTParty.get(url, :query => params, :headers => headers)
60
+ elsif request_type == "POST"
61
+ if headers.key?("Content-Type") and headers["Content-Type"] == "application/json"
62
+ params = JSON.generate(params)
63
+ end
64
+ response = HTTParty.post(url, :body => params, :headers => headers)
65
+ else
66
+ puts "Request Type #{request_type} unsupported"
67
+ return
68
+ end
69
+
70
+ if to_json
71
+ return JSON.parse(response.body)
72
+ else
73
+ return response.body
74
+ end
75
+ end
76
+
77
+
78
+ ### API CALLS ###
79
+
80
+ # Get details for a specific patient
81
+ def get_patient(patient_id)
82
+ SuperEHR.not_implemented(__callee__)
83
+ end
84
+
85
+ # Gets a list of patients
86
+ def get_patients
87
+ SuperEHR.not_implemented(__callee__)
88
+ end
89
+
90
+ # Get a list of patients changed since ts
91
+ def get_changed_patients(ts)
92
+ SuperEHR.not_implemented(__callee__)
93
+ end
94
+
95
+ # Get a list of patients changed since ts
96
+ def get_changed_patients_ids(ts)
97
+ SuperEHR.not_implemented(__callee__)
98
+ end
99
+
100
+ # Get patients scheduled for a specific day
101
+ def get_scheduled_patients(day)
102
+ SuperEHR.not_implemented(__callee__)
103
+ end
104
+
105
+ # upload a pdf
106
+ def upload_document(patient_id, filepath, description)
107
+ SuperEHR.not_implemented(__callee__)
108
+ end
109
+ end
110
+
111
+ class AllScriptsAPI < BaseEHR
112
+
113
+ ### API SPECIFIC HOUSEKEEPING ###
114
+
115
+ def initialize(ehr_username, ehr_password='',
116
+ app_username, app_password, app_name,
117
+ using_touchworks)
118
+
119
+ # convert these to environment variables
120
+ @app_username = app_username
121
+ @app_password = app_password
122
+ @app_name = app_name
123
+
124
+ @ehr_username = ehr_username
125
+ # if not using touchworks ehr, then professional ehr is used
126
+ @using_touchworks = using_touchworks
127
+
128
+ if (using_touchworks)
129
+ base_url = "http://twlatestga.unitysandbox.com/unity/unityservice.svc"
130
+ else
131
+ base_url = "http://pro141ga.unitysandbox.com/Unity/unityservice.svc"
132
+ end
133
+
134
+ @uri = URI(base_url)
135
+ end
136
+
137
+ def get_default_params
138
+ return {:Action => '', :AppUserID => @ehr_username, :Appname => @app_name,
139
+ :PatientID => '', :Token => refresh_token,
140
+ :Parameter1 => '', :Parameter2 => '', :Parameter3 => '',
141
+ :Parameter4 => '', :Parameter5 => '', :Parameter6 => '', :Data => ''}
142
+ end
143
+
144
+ def refresh_token
145
+ credentials = {:Username => @app_username, :Password => @app_password}
146
+ # last two params prevents usage of default params and output to json
147
+ return make_request("POST", "json/GetToken", credentials, false, false)
148
+ end
149
+
150
+ def get_request_headers
151
+ return { 'Content-Type' => 'application/json' }
152
+ end
153
+
154
+ def get_request_url(endpoint)
155
+ return "#{@uri}/#{endpoint}"
156
+ end
157
+
158
+ ### API CALLS ###
159
+
160
+ def get_patient(patient_id)
161
+ params = {:Action => 'GetPatient', :PatientID => patient_id}
162
+ response = make_request("POST", "json/MagicJson", params)[0]
163
+ patient_info = {}
164
+ if response.key?("getpatientinfo")
165
+ if not response["getpatientinfo"].empty?
166
+ patient_info = response["getpatientinfo"][0]
167
+ end
168
+ end
169
+ return patient_info
170
+ end
171
+
172
+ def get_changed_patients(ts='')
173
+ patient_ids = get_changed_patients_ids(ts)
174
+ patients = []
175
+ for id in patient_ids
176
+ patients << get_patient(id)
177
+ end
178
+ return patients
179
+ end
180
+
181
+ def get_changed_patients_ids(ts='')
182
+ params = {:Action => 'GetChangedPatients', :Parameter1 => ts}
183
+ response = make_request("POST", "json/MagicJson", params)[0]
184
+ patient_ids = []
185
+ if response.key?("getchangedpatientsinfo")
186
+ patient_ids = response["getchangedpatientsinfo"].map {|x| x["patientid"] }
187
+ end
188
+ return patient_ids
189
+ end
190
+
191
+ def get_scheduled_patients(day)
192
+ params = {:Action => 'GetSchedule', :Parameter1 => day}
193
+ response = make_request("POST", "json/MagicJson", params)[0]
194
+ patients = []
195
+ if response.key?("getscheduleinfo")
196
+ if not response["getscheduleinfo"].empty?
197
+ for scheduled_patient in response["getscheduleinfo"]
198
+ patients << scheduled_patient
199
+ end
200
+ end
201
+ end
202
+ return patients
203
+ end
204
+
205
+ ## UPLOAD PDF IMPLEMENTATION ##
206
+
207
+ def upload_document(patient_id, filepath, description)
208
+
209
+ patient = get_patient(patient_id)
210
+ first_name = patient["Firstname"]
211
+ last_name = patient["LastName"]
212
+
213
+ File.open(filepath, "r:UTF-8") do |file|
214
+ file_contents = file.read()
215
+ buffer = Base64.encode64(file_contents)
216
+
217
+ # first call to push the contents
218
+ save_pdf_xml = create_pdf_xml_params(first_name, last_name,
219
+ filepath, file.size, 0, "false", "", "0")
220
+ params = {:Action => 'SaveDocumentImage', :PatientID => patient_id,
221
+ :Parameter1 => save_pdf_xml, :Parameter6 => buffer}
222
+ out = make_request("POST", "json/MagicJson", params)
223
+ # second call to push file information and wrap up upload
224
+ doc_guid = out[0]["savedocumentimageinfo"][0]["documentVar"].to_s
225
+ save_pdf_xml = create_pdf_xml_params(first_name, last_name,
226
+ filepath, file.size, 0, "true", doc_guid, "0")
227
+ params = {:Action => 'SaveDocumentImage', :PatientID => patient_id,
228
+ :Parameter1 => save_pdf_xml, :Parameter6 => nil}
229
+ out = make_request("POST", "json/MagicJson", params)
230
+ return out
231
+ end
232
+ end
233
+
234
+ # create XML parameters needed for upload_document
235
+ def create_pdf_xml_params(first_name, last_name, file_name, bytes_read,
236
+ offset, upload_done, docs_guid, encounter_id, organization_name="TouchWorks")
237
+ xml = Builder::XmlMarkup.new(:indent => 2)
238
+ xml.doc do |b|
239
+ b.item :name => "documentCommand", :value => "i"
240
+ b.item :name => "documentType", :value => (@using_touchworks ? "sEKG" : "1")
241
+ b.item :name => "offset", :value => offset
242
+ b.item :name => "bytesRead", :value => bytes_read
243
+ b.item :name => "bDoneUpload", :value => upload_done
244
+ b.item :name => "documentVar", :value => docs_guid
245
+ b.item :name => "vendorFileName", :value => file_name
246
+ b.item :name => "ahsEncounterID", :value => 0
247
+ b.item :name => "ownerCode", :value => get_provider_entry_code.strip
248
+ b.item :name => "organizationName", :value => organization_name
249
+ b.item :name => "patientFirstName", :value => first_name
250
+ b.item :name => "patientLastName", :value => last_name
251
+ end
252
+ return xml.target!
253
+ end
254
+
255
+ private
256
+
257
+ # necessary to create the xml params for upload_document
258
+ def get_provider_entry_code()
259
+ params = {:Action => 'GetProvider', :Parameter2 => @ehr_username}
260
+ out = make_request("POST", "json/MagicJson", params)
261
+ return out[0]["getproviderinfo"][0]["EntryCode"]
262
+ end
263
+
264
+ def get_encounter(patient_id)
265
+ params = {:Action => 'GetEncounter', :PatientID => patient_id, :Parameter1 => "NonAppt",
266
+ :Parameter2 => Time.new.strftime("%b %d %Y %H:%M:%S"), :Parameter3 => true,
267
+ :Parameter4 => 'N'}
268
+ out = make_request("POST", "json/MagicJson", params)
269
+ return out[0]["getencounterinfo"][0]["EncounterID"]
270
+ end
271
+ end
272
+
273
+ class AthenaAPI < BaseEHR
274
+
275
+ ### API SPECIFIC HOUSEKEEPING ###
276
+
277
+ def initialize(version, key, secret, practice_id)
278
+ @uri = URI.parse('https://api.athenahealth.com/')
279
+ @version = version
280
+ @key = key
281
+ @secret = secret
282
+ @practiceid = practice_id
283
+ end
284
+
285
+ def get_request_headers
286
+ return { 'Authorization' => "Bearer #{refresh_token}" }
287
+ end
288
+
289
+ def get_request_url(endpoint)
290
+ return "#{@uri}/#{@version}/#{@practiceid}/#{endpoint}"
291
+ end
292
+
293
+ def refresh_token
294
+ auth_paths = {
295
+ 'vi' => 'oauth',
296
+ 'preview1' => 'oauthpreview',
297
+ 'openpreview1' => 'oauthopenpreview',
298
+ }
299
+
300
+ auth = {:username => @key, :password => @secret}
301
+
302
+ url = "#{@uri}/#{auth_paths[@version]}/token"
303
+ params = {:grant_type => "client_credentials"}
304
+ response = HTTParty.post(url, :body => params, :basic_auth => auth)
305
+
306
+ return response["access_token"]
307
+ end
308
+
309
+
310
+ ### API CALLS ###
311
+
312
+ def get_patient(patient_id)
313
+ response = make_request("GET", "patients/#{patient_id}", {})
314
+ patient_info = {}
315
+ if not response[0].empty?
316
+ patient_info = response[0]
317
+ end
318
+ return patient_info
319
+ end
320
+
321
+ def get_changed_patients(ts='')
322
+ patient_ids = get_changed_patients_ids(ts)
323
+ patients = []
324
+ for id in patient_ids
325
+ patients << get_patient(id)
326
+ end
327
+ return patients
328
+ end
329
+
330
+ # start_date needs to be in mm/dd/yyyy
331
+ # returns a list of patient ids that have been changed since start_date
332
+ def get_changed_patients_ids(start_date,
333
+ end_date=Time.new.strftime("%m/%d/%Y %H:%M:%S"))
334
+ subscribe = make_request("GET", "patients/changed/subscription", {})
335
+ if subscribe.has_key?("status") and subscribe["status"] == "ACTIVE"
336
+ response = make_request("GET", "patients/changed",
337
+ { :ignorerestrictions => false,
338
+ :leaveunprocessed => false,
339
+ :showprocessedstartdatetime => "#{start_date} 00:00:00",
340
+ :showprocessedenddatetime => end_date })
341
+ patient_ids = []
342
+ if response.key?("patients")
343
+ patient_ids = response["patients"].map {|x| x["patientid"] }
344
+ end
345
+ return patient_ids
346
+ else
347
+ return nil
348
+ end
349
+ end
350
+
351
+ def get_scheduled_patients(date, department_id=1)
352
+ response = make_request("GET", "appointments/booked",
353
+ {:departmentid => department_id, :startdate => date, :enddate => date})
354
+ patients = []
355
+ if not response["appointments"].empty?
356
+ for scheduled_patient in response["appointments"]
357
+ patients << scheduled_patient
358
+ end
359
+ end
360
+ return patients
361
+ end
362
+
363
+ # might have issues if patient is in multiple departments
364
+ def upload_document(patient_id, filepath, description, department_id=-1)
365
+ endpoint = "patients/#{patient_id}/documents"
366
+ headers = { 'Authorization' => "Bearer #{refresh_token}" }
367
+ url = "#{@uri}/#{@version}/#{@practiceid}/#{endpoint}"
368
+ params = {
369
+ :departmentid => department_id != -1 ? department_id : get_patient(patient_id)["departmentid"],
370
+ :attachmentcontents => File.new(filepath),
371
+ :documentsubclass => "CLINICALDOCUMENT",
372
+ :autoclose => false,
373
+ :internalnote => description,
374
+ :actionnote => description }
375
+
376
+ response = HTTMultiParty.post(url, :body => params, :headers => headers)
377
+ return response
378
+ end
379
+
380
+ end
381
+
382
+ class DrChronoAPI < BaseEHR
383
+
384
+ ### API SPECIFIC HOUSEKEEPING ###
385
+
386
+ def initialize(access_code, client_id, client_secret, redirect_uri)
387
+ @access_code = access_code
388
+ @client_id = client_id
389
+ @client_secret = client_secret
390
+ @redirect_uri = redirect_uri << '/' unless redirect_uri.end_with?('/')
391
+ @access_token = ''
392
+ @refresh_token = ''
393
+ @uri = URI.parse("https://drchrono.com")
394
+ if (access_code == '')
395
+ get_access_token
396
+ else
397
+ refresh_token
398
+ end
399
+ end
400
+
401
+ def get_request_headers
402
+ return { 'Authorization' => "Bearer #{refresh_token}" }
403
+ end
404
+
405
+ def get_request_url(endpoint)
406
+ return "#{@uri}/#{endpoint}"
407
+ end
408
+
409
+ def refresh_token
410
+ if @refresh_token == ''
411
+ puts get_request_url("o/token")
412
+ puts @redirect_uri
413
+ response = HTTParty.post(get_request_url("o/token/"),
414
+ :body => {:code => @access_code,
415
+ :grant_type => "authorization_code",
416
+ :redirect_uri => @redirect_uri,
417
+ :client_id => @client_id,
418
+ :client_secret => @client_secret})
419
+ @refresh_token = response["refresh_token"]
420
+ @access_token = response["access_token"]
421
+ return response["access_token"]
422
+ else
423
+ response = HTTParty.post(get_request_url("o/token/"),
424
+ :body => {:refresh_token => @refresh_token,
425
+ :grant_type => "refresh_token",
426
+ :redirect_uri => @redirect_uri,
427
+ :client_id => @client_id,
428
+ :client_secret => @client_secret})
429
+ @refresh_token = response["refresh_token"]
430
+ @access_token = response["access_token"]
431
+ return response["access_token"]
432
+ end
433
+
434
+ end
435
+
436
+ def chrono_request(endpoint, params={})
437
+ result = []
438
+ while endpoint
439
+ data = make_request("GET", endpoint, params)
440
+ if data["results"]
441
+ result = result | data["results"]
442
+ end
443
+ endpoint = data["next"]
444
+ end
445
+ return result
446
+ end
447
+
448
+ ### API CALLS ###
449
+
450
+ # Not efficient
451
+ # Get the patient using patient id from our database
452
+ def get_patient(patient_id)
453
+ patients = get_patients()
454
+ for patient in patients
455
+ if patient["id"] == patient_id
456
+ return patient
457
+ end
458
+ end
459
+ return nil
460
+ end
461
+
462
+ def get_patients(params={})
463
+ patient_url = 'api/patients'
464
+ return chrono_request(patient_url, params)
465
+ end
466
+
467
+ def get_changed_patients(ts)
468
+ date = ts.gsub(/\//, '-')
469
+ date = Date.strptime(date, '%m-%d-%Y')
470
+ return get_patients({:since => date.iso8601})
471
+ end
472
+
473
+ def get_changed_patients_ids(ts)
474
+ patients = get_changed_patients(ts)
475
+ ids = []
476
+ for patient in patients
477
+ ids << patient["id"]
478
+ end
479
+ return ids
480
+ end
481
+
482
+ def get_scheduled_patients(day='')
483
+ url = 'api/appointments'
484
+ return chrono_request(url, {:date => day.gsub(/\//, '-')})
485
+ end
486
+
487
+ def upload_document(patient_id, filepath, description)
488
+ url = get_request_url("api/documents")
489
+ headers = get_request_headers
490
+ params = {
491
+ :doctor => /\/api\/doctors\/.*/.match(get_patient(patient_id)["doctor"]),
492
+ :patient => "/api/patients/#{patient_id}",
493
+ :description => description,
494
+ :date => Time.now.strftime("%Y-%m-%d") << " 00:00:00",
495
+ :document => File.new(filepath)
496
+ }
497
+ response = HTTMultiParty.post(url, :body => params, :headers => headers)
498
+ return response
499
+ end
500
+
501
+ end
502
+
503
+ def self.allscripts(ehr_username, ehr_password,
504
+ app_username, app_password, app_name,
505
+ using_touchworks)
506
+ return AllScriptsAPI.new(ehr_username, ehr_password,
507
+ app_username, app_password, app_name,
508
+ using_touchworks)
509
+ end
510
+
511
+ def self.athena(version, key, secret, practice_id)
512
+ return AthenaAPI.new(version, key, secret, practice_id)
513
+ end
514
+
515
+ def self.drchrono(access_code, client_id, client_secret, redirect_uri)
516
+ return DrChronoAPI.new(access_code, client_id, client_secret, redirect_uri)
517
+ end
518
+ end
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: super_ehr
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brian Su
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-28 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: This project generalizes EHR integrations with various EHR vendors. Currently
14
+ supports Allscripts, Athena, and DrChrono.
15
+ email: brian@bsu.me
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/super_ehr.rb
21
+ homepage: http://rubygems.org/gems/super_ehr
22
+ licenses:
23
+ - MIT
24
+ metadata: {}
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ requirements: []
40
+ rubyforge_project:
41
+ rubygems_version: 2.4.3
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: Integrate with various EHR APIs seamlessly.
45
+ test_files: []