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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3f952383908647ed314df37a42487c17d0c8d2a6ae648d782e1ba3f7963b68bc
4
+ data.tar.gz: 6813646ff087268ac86ede1a612bbd7a629fb7995eb27fbf6766fc2ccde56929
5
+ SHA512:
6
+ metadata.gz: d4fa530c8a5e46a1e050eddf7ddc79c16ceeb5a0aa77aa4f3fef3908c71f1d7818aa70930af39f74e78fe545bc232b9b4a94bf115a8cb8843f4c057fd7fd878a
7
+ data.tar.gz: aa1ca19284efd2606d211f23c137d4293c88799fa074fefc3c568d54aee8fba70286c18b5ab48c86a31108cbf3efaf430a92728c28553d8292ebe3c0e7b0bafe
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
+ # DdeMahis
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,94 @@
1
+ module DdeMahis
2
+ class Api::V1::DdeController < ApplicationController
3
+ # GET /dde/patients
4
+ def find_patients_by_npid
5
+ npid = params.require(:npid)
6
+ render json: service.find_patients_by_npid(npid)
7
+ end
8
+
9
+ def find_patients_by_name_and_gender
10
+ given_name, family_name, gender = params.require(%i[given_name family_name gender])
11
+ render json: service.find_patients_by_name_and_gender(given_name, family_name, gender)
12
+ end
13
+
14
+ def import_patients_by_npid
15
+ npid = params.require(:npid)
16
+ render json: service.import_patients_by_npid(npid)
17
+ end
18
+
19
+ def import_patients_by_doc_id
20
+ doc_id = params.require(:doc_id)
21
+ render json: service.import_patients_by_doc_id(doc_id)
22
+ end
23
+
24
+ def remaining_npids
25
+ render json: service.remaining_npids
26
+ end
27
+
28
+ # GET /api/v1/dde/match
29
+ #
30
+ # Returns DdeMahis patients matching demographics passed
31
+ def match_patients_by_demographics
32
+ render json: service.match_patients_by_demographics(
33
+ given_name: match_params[:given_name],
34
+ family_name: match_params[:family_name],
35
+ gender: match_params[:gender],
36
+ birthdate: match_params[:birthdate],
37
+ home_traditional_authority: match_params[:home_traditional_authority],
38
+ home_district: match_params[:home_district],
39
+ home_village: match_params[:home_village]
40
+ )
41
+ end
42
+
43
+ def reassign_patient_npid
44
+ patient_ids = params.permit(:doc_id, :patient_id)
45
+ render json: service.reassign_patient_npid(patient_ids)
46
+ end
47
+
48
+ def merge_patients
49
+ primary_patient_ids = params.require(:primary)
50
+ secondary_patient_ids_list = params.require(:secondary)
51
+
52
+ render json: service.merge_patients(primary_patient_ids, secondary_patient_ids_list)
53
+ end
54
+
55
+ def patient_diff
56
+ patient_id = params.require(:patient_id)
57
+ diff = service.find_patient_updates(patient_id)
58
+
59
+ render json: { diff: diff }
60
+ end
61
+
62
+ ##
63
+ # Updates local patient with demographics in DdeMahis.
64
+ def refresh_patient
65
+ patient_id = params.require(:patient_id)
66
+ update_npid = params[:update_npid]&.casecmp?('true') || false
67
+
68
+ patient = service.update_local_patient(Patient.find(patient_id), update_npid: update_npid)
69
+
70
+ render json: patient
71
+ end
72
+
73
+ private
74
+
75
+ MATCH_PARAMS = %i[given_name family_name gender birthdate home_village
76
+ home_traditional_authority home_district].freeze
77
+
78
+ def match_params
79
+ MATCH_PARAMS.each_with_object({}) do |param, params_hash|
80
+ raise "param #{param} is required" if params[param].blank?
81
+
82
+ params_hash[param] = params[param]
83
+ end
84
+ end
85
+
86
+ def service
87
+ DdeService.new(visit_type: visit_type)
88
+ end
89
+
90
+ def visit_type
91
+ Program.find(params.require(:visit_type_id))
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # controller for managing merge rollback
4
+ module DdeMahis
5
+ module Api
6
+ module V1
7
+ class RollbackController < ApplicationController
8
+ def merge_history
9
+ identifier = params.require(:identifier)
10
+ render json: merge_service.get_patient_audit(identifier), status: :ok
11
+ end
12
+
13
+ def rollback_patient
14
+ patient_id = params.require(:patient_id)
15
+ visit_type_id = params.require(:visit_type_id)
16
+ render json: rollback_service.rollback_merged_patient(patient_id, visit_type_id), status: :ok
17
+ end
18
+
19
+ private
20
+
21
+ def merge_service
22
+ MergeAuditService.new
23
+ end
24
+
25
+ def rollback_service
26
+ RollbackService.new
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,4 @@
1
+ module DdeMahis
2
+ class ApplicationController < ActionController::API
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module DdeMahis
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module DdeMahis
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module DdeMahis
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module DdeMahis
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 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 DdeMahis 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 DdeMahis
17
+ def connect(url:, username:, password:)
18
+ @connection = establish_connection(url: url, username: username, password: password)
19
+ end
20
+
21
+ # Reconnect to DdeMahis using previous connection
22
+ #
23
+ # @see: DdeMahisClient#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
+ DdeMahis_API_KEY_VALIDITY_PERIOD = 3600 * 12
57
+ DdeMahis_VERSION = 'v1'
58
+
59
+ # Reload old connection to DdeMahis
60
+ def reload_connection(connection)
61
+ LOGGER.debug 'Loading DdeMahis connection'
62
+ if connection[:expires] < Time.now
63
+ LOGGER.debug 'DdeMahis 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 DdeMahis
72
+ #
73
+ # NOTE: This simply involves logging into DdeMahis
74
+ def establish_connection(url:, username:, password:)
75
+ LOGGER.debug 'Establishing new connection to DdeMahis 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 DdeMahis: #{response}"
93
+ end
94
+
95
+ LOGGER.info('Connection to DdeMahis established :)')
96
+ @connection = {
97
+ key: response['access_token'],
98
+ expires: Time.now + DdeMahis_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}/#{DdeMahis_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 DdeMahis request (#{resource})"
117
+ response = yield build_uri(resource), headers
118
+ LOGGER.debug "Handling DdeMahis response:\n\tStatus - #{response.code}\n\tBody - #{response.body}"
119
+ handle_response response
120
+ rescue RestClient::Unauthorized => e
121
+ LOGGER.error "DdeMahisClient suppressed exception: #{e}"
122
+ return handle_response e.response unless @auto_login
123
+
124
+ LOGGER.debug 'Auto-logging into DdeMahis...'
125
+ establish_connection(@connection[:config])
126
+ LOGGER.debug "Reset connection: #{@connection}"
127
+ retry # Retry last request...
128
+ rescue RestClient::BadRequest => e
129
+ LOGGER.error "DdeMahisClient suppressed exception: #{e}"
130
+ handle_response e.response
131
+ rescue RestClient::UnprocessableEntity => e
132
+ LOGGER.error "DdeMahisClient suppressed exception: #{e}"
133
+ handle_response e.response
134
+ rescue RestClient::NotFound => e
135
+ LOGGER.error "DdeMahisClient suppressed exception: #{e}"
136
+ handle_response e.response
137
+ rescue RestClient::InternalServerError => e
138
+ LOGGER.error "DdeMahisClient 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 DdeMahis 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
+ # DdeMahis 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