mahis-dde 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7505492de7f9f3257c9b711cf894ec77cf708fee5562d060c415e341caf2ad65
4
+ data.tar.gz: 322ae74b18e2f6e1c73b48a0c5bed16374c945172370388e982b8c327c621a21
5
+ SHA512:
6
+ metadata.gz: 4896294257cd71d17c9a60841643dce73f2c7a72bd7f882984ba6db1687399f1141cfacd98aa7c78fc1f56c5a97b07614769755ec3106cdf1748583f15b4516a
7
+ data.tar.gz: 6b079c31529e9cec84a2e90a5fda87e1756c642a0c5f9568ff9401ca96b91d97ea1352046f7848e1d5300115dbd0c86bf1a423b639ec4d94b33a55c172150710
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2024 bryan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # Dde
2
+ DDE stands for Demographics Data Exchange. Its main purpose is to manage patient IDs
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'dde', git: 'https://github.com/Malawi-Ministry-of-Health/dde-client', branch: 'main'
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle install
17
+ ```
18
+
19
+ ## License
20
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/dde .css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,92 @@
1
+ class Dde::Api::V1::DdeController < ApplicationController
2
+ # GET /dde/patients
3
+ def find_patients_by_npid
4
+ npid = params.require(:npid)
5
+ render json: service.find_patients_by_npid(npid)
6
+ end
7
+
8
+ def find_patients_by_name_and_gender
9
+ given_name, family_name, gender = params.require(%i[given_name family_name gender])
10
+ render json: service.find_patients_by_name_and_gender(given_name, family_name, gender)
11
+ end
12
+
13
+ def import_patients_by_npid
14
+ npid = params.require(:npid)
15
+ render json: service.import_patients_by_npid(npid)
16
+ end
17
+
18
+ def import_patients_by_doc_id
19
+ doc_id = params.require(:doc_id)
20
+ render json: service.import_patients_by_doc_id(doc_id)
21
+ end
22
+
23
+ def remaining_npids
24
+ render json: service.remaining_npids
25
+ end
26
+
27
+ # GET /api/v1/dde/match
28
+ #
29
+ # Returns Dde patients matching demographics passed
30
+ def match_patients_by_demographics
31
+ render json: service.match_patients_by_demographics(
32
+ given_name: match_params[:given_name],
33
+ family_name: match_params[:family_name],
34
+ gender: match_params[:gender],
35
+ birthdate: match_params[:birthdate],
36
+ home_traditional_authority: match_params[:home_traditional_authority],
37
+ home_district: match_params[:home_district],
38
+ home_village: match_params[:home_village]
39
+ )
40
+ end
41
+
42
+ def reassign_patient_npid
43
+ patient_ids = params.permit(:doc_id, :patient_id)
44
+ render json: service.reassign_patient_npid(patient_ids)
45
+ end
46
+
47
+ def merge_patients
48
+ primary_patient_ids = params.require(:primary)
49
+ secondary_patient_ids_list = params.require(:secondary)
50
+
51
+ render json: service.merge_patients(primary_patient_ids, secondary_patient_ids_list)
52
+ end
53
+
54
+ def patient_diff
55
+ patient_id = params.require(:patient_id)
56
+ diff = service.find_patient_updates(patient_id)
57
+
58
+ render json: { diff: diff }
59
+ end
60
+
61
+ ##
62
+ # Updates local patient with demographics in Dde.
63
+ def refresh_patient
64
+ patient_id = params.require(:patient_id)
65
+ update_npid = params[:update_npid]&.casecmp?('true') || false
66
+
67
+ patient = service.update_local_patient(Patient.find(patient_id), update_npid: update_npid)
68
+
69
+ render json: patient
70
+ end
71
+
72
+ private
73
+
74
+ MATCH_PARAMS = %i[given_name family_name gender birthdate home_village
75
+ home_traditional_authority home_district].freeze
76
+
77
+ def match_params
78
+ MATCH_PARAMS.each_with_object({}) do |param, params_hash|
79
+ raise "param #{param} is required" if params[param].blank?
80
+
81
+ params_hash[param] = params[param]
82
+ end
83
+ end
84
+
85
+ def service
86
+ Dde::DdeService.new(visit_type: visit_type)
87
+ end
88
+
89
+ def visit_type
90
+ Program.find(params.require(:visit_type_id))
91
+ end
92
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # controller for managing merge rollback
4
+ class Api::V1::RollbackController < ApplicationController
5
+ def merge_history
6
+ identifier = params.require(:identifier)
7
+ render json: merge_service.get_patient_audit(identifier), status: :ok
8
+ end
9
+
10
+ def rollback_patient
11
+ patient_id = params.require(:patient_id)
12
+ visit_type_id = params.require(:visit_type_id)
13
+ render json: rollback_service.rollback_merged_patient(patient_id, visit_type_id), status: :ok
14
+ end
15
+
16
+ private
17
+
18
+ def merge_service
19
+ MergeAuditService.new
20
+ end
21
+
22
+ def rollback_service
23
+ RollbackService.new
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ module Dde
2
+ class ApplicationController < ActionController::API
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Dde
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Dde
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Dde
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Dde
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'restclient'
5
+
6
+ class Dde::DdeClient
7
+ def initialize
8
+ @auto_login = true # If logged out, automatically login on next request
9
+ @base_url = nil
10
+ @connection = nil
11
+ end
12
+
13
+ # Connect to Dde Web Service using either a configuration file
14
+ # or an old Connection.
15
+ #
16
+ # @return A Connection object that can be used to re-connect to Dde
17
+ def connect(url:, username:, password:)
18
+ @connection = establish_connection(url: url, username: username, password: password)
19
+ end
20
+
21
+ # Reconnect to Dde using previous connection
22
+ #
23
+ # @see: DdeClient#connect
24
+ def restore_connection(connection)
25
+ @connection = reload_connection(connection)
26
+ end
27
+
28
+ def get(resource)
29
+ exec_request resource do |url, headers|
30
+ RestClient.get url, headers
31
+ end
32
+ end
33
+
34
+ def post(resource, data)
35
+ exec_request resource do |url, headers|
36
+ RestClient.post url, data.to_json, headers
37
+ end
38
+ end
39
+
40
+ def put(resource, data)
41
+ exec_request resource do |url, headers|
42
+ RestClient.put url, data.to_json, headers
43
+ end
44
+ end
45
+
46
+ def delete(resource)
47
+ exec_request resource do |url, headers|
48
+ RestClient.delete url, headers
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ JSON_CONTENT_TYPE = 'application/json'
55
+ LOGGER = Logger.new(STDOUT)
56
+ Dde_API_KEY_VALIDITY_PERIOD = 3600 * 12
57
+ Dde_VERSION = 'v1'
58
+
59
+ # Reload old connection to Dde
60
+ def reload_connection(connection)
61
+ LOGGER.debug 'Loading Dde connection'
62
+ if connection[:expires] < Time.now
63
+ LOGGER.debug 'Dde connection expired'
64
+ establish_connection(connection[:config])
65
+ else
66
+ @base_url = connection[:config][:url]
67
+ connection
68
+ end
69
+ end
70
+
71
+ # Establish a connection to Dde
72
+ #
73
+ # NOTE: This simply involves logging into Dde
74
+ def establish_connection(url:, username:, password:)
75
+ LOGGER.debug 'Establishing new connection to Dde from configuration'
76
+
77
+ # Block any automatic logins when processing request to avoid infinite loop
78
+ # in request execution below... Under normal circumstances request execution
79
+ # will attempt a login if 401 is met. Not pretty, I know but it does the job
80
+ # for now!!!
81
+ @auto_login = false
82
+
83
+ # HACK: Globally save base_url as a connection object may not currently
84
+ # be available to the build_url method right now
85
+ @base_url = url
86
+
87
+ response, status = post('login', username: username, password: password)
88
+
89
+ @auto_login = true
90
+
91
+ if status != 200
92
+ raise StandardError, "Unable to establish connection to Dde: #{response}"
93
+ end
94
+
95
+ LOGGER.info('Connection to Dde established :)')
96
+ @connection = {
97
+ key: response['access_token'],
98
+ expires: Time.now + Dde_API_KEY_VALIDITY_PERIOD,
99
+ config: { url: url, username: username, password: password }
100
+ }
101
+ end
102
+
103
+ # Returns a URI object with API host attached
104
+ def build_uri(resource)
105
+ "#{@base_url}/#{Dde_VERSION}/#{resource}"
106
+ end
107
+
108
+ def headers
109
+ {
110
+ 'Content-type' => JSON_CONTENT_TYPE,
111
+ 'Authorization' => @connection ? @connection[:key] : nil
112
+ }
113
+ end
114
+
115
+ def exec_request(resource)
116
+ LOGGER.debug "Executing Dde request (#{resource})"
117
+ response = yield build_uri(resource), headers
118
+ LOGGER.debug "Handling Dde response:\n\tStatus - #{response.code}\n\tBody - #{response.body}"
119
+ handle_response response
120
+ rescue RestClient::Unauthorized => e
121
+ LOGGER.error "DdeClient suppressed exception: #{e}"
122
+ return handle_response e.response unless @auto_login
123
+
124
+ LOGGER.debug 'Auto-logging into Dde...'
125
+ establish_connection(@connection[:config])
126
+ LOGGER.debug "Reset connection: #{@connection}"
127
+ retry # Retry last request...
128
+ rescue RestClient::BadRequest => e
129
+ LOGGER.error "DdeClient suppressed exception: #{e}"
130
+ handle_response e.response
131
+ rescue RestClient::UnprocessableEntity => e
132
+ LOGGER.error "DdeClient suppressed exception: #{e}"
133
+ handle_response e.response
134
+ rescue RestClient::NotFound => e
135
+ LOGGER.error "DdeClient suppressed exception: #{e}"
136
+ handle_response e.response
137
+ rescue RestClient::InternalServerError => e
138
+ LOGGER.error "DdeClient suppressed exceptionnnn: #{e}"
139
+ handle_response e.response
140
+ end
141
+
142
+ def handle_response(response)
143
+ # 204 is no content response, no further processing required.
144
+ return nil, 204 if response.code.to_i == 204
145
+
146
+ # NOTE: Following is commented out as Dde at the moment is quite liberal
147
+ # in how it responds to various requests. It seems to know no difference
148
+ # between 'application/json' and 'text/plain'.
149
+ #
150
+ # unless response["content-type"].include? JSON_CONTENT_TYPE
151
+ # puts "Invalid response from API: content-type: " + response["content-type"]
152
+ # return nil, 0
153
+ # end
154
+
155
+ # Dde is somewhat undecided on how it reports back its status code.
156
+ # Sometimes we get a proper HTTP status code and sometimes it is within
157
+ # the response body.
158
+ # response_status = response.code || response.body['status']
159
+ response_status = response.code
160
+ [JSON.parse(response.body), response_status&.to_i]
161
+ end
162
+ end