malawi_hiv_program_reports 1.0.15 → 1.0.17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/services/malawi_hiv_program_reports/clinic/patients_alive_and_on_treatment.rb +1 -1
- data/app/services/malawi_hiv_program_reports/clinic/regimens_by_weight_and_gender.rb +1 -1
- data/app/services/malawi_hiv_program_reports/moh/{cohort.rb → art_cohort.rb} +9 -2
- data/app/services/malawi_hiv_program_reports/moh/cumulative_cohort.rb +399 -0
- data/app/services/malawi_hiv_program_reports/moh/cumulative_outcome.rb +545 -0
- data/app/services/malawi_hiv_program_reports/report_map.rb +4 -2
- data/app/services/malawi_hiv_program_reports/utils/common_sql_query_utils.rb +6 -0
- data/lib/malawi_hiv_program_reports/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2528255432f2df888bfc40d425af064647391095c2a0ceebe9512e348fbe2f63
|
4
|
+
data.tar.gz: 75916f779053f8acf6e1aa4ef258668961266eff12eadd92bdd4cd3cbf3e5a7f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7f98b4442247cc8fce9df43887e4ee9cccfdae8017d15fd8f67a301c89428cf54a1c1e411c9e61fc4c56f99de41a45a2732303063ecf7b17a18b46efedba8aa3
|
7
|
+
data.tar.gz: c3d0300aa8341c5bc387cf3ff89b854012d2b093cd058453278fbb378129b0e29ddd650a973d53d0e830953779e922e5a81a5c214006b7cbc53788473a86fa19
|
@@ -8,7 +8,7 @@ module MalawiHivProgramReports
|
|
8
8
|
# the constructor. This method must be called to build report and save
|
9
9
|
# it to database.
|
10
10
|
# rubocop:disable Metrics/ClassLength
|
11
|
-
class
|
11
|
+
class ArtCohort
|
12
12
|
include MalawiHivProgramReports::Utils::ConcurrencyUtils
|
13
13
|
include MalawiHivProgramReports::Utils::ModelUtils
|
14
14
|
include MalawiHivProgramReports::Adapters::Moh::Custom
|
@@ -30,8 +30,8 @@ module MalawiHivProgramReports
|
|
30
30
|
def build_report
|
31
31
|
with_lock(LOCK_FILE, blocking: false) do
|
32
32
|
init_drill_down_table
|
33
|
-
clear_drill_down
|
34
33
|
@cohort_builder.build(@cohort_struct, @start_date, @end_date, @occupation)
|
34
|
+
clear_drill_down
|
35
35
|
save_report
|
36
36
|
end
|
37
37
|
rescue ::FailedToAcquireLock => e
|
@@ -152,6 +152,12 @@ module MalawiHivProgramReports
|
|
152
152
|
|
153
153
|
LOGGER = Rails.logger
|
154
154
|
|
155
|
+
def find_saved_report
|
156
|
+
result = ::Report.where(type: @type, name: "#{@name}#{"-#{@occupation}"}#{"-#{@location_name}"}",
|
157
|
+
start_date: @start_date, end_date: @end_date)
|
158
|
+
result&.map { |r| r['id'] } || []
|
159
|
+
end
|
160
|
+
|
155
161
|
# Writes the report to database
|
156
162
|
def save_report
|
157
163
|
::Report.transaction do
|
@@ -196,6 +202,7 @@ module MalawiHivProgramReports
|
|
196
202
|
def clear_drill_down
|
197
203
|
ActiveRecord::Base.connection.execute <<~SQL
|
198
204
|
DELETE FROM cohort_drill_down #{site_manager(operator: 'WHERE', column: 'site_id', location: @location)}
|
205
|
+
#{find_saved_report.count.positive? ? "AND reporting_report_design_resource_id IN (#{find_saved_report.join(',')})" : ''}
|
199
206
|
SQL
|
200
207
|
rescue StandardError => e
|
201
208
|
ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS cohort_drill_down;'
|
@@ -0,0 +1,399 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MalawiHivProgramReports
|
4
|
+
module Moh
|
5
|
+
# This is the Cumulative Cohort Builder class
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
7
|
+
class CumulativeCohort
|
8
|
+
include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
|
9
|
+
|
10
|
+
attr_reader :start_date, :end_date, :locations, :rebuild, :outcome, :definition
|
11
|
+
|
12
|
+
LOCK_FILE = 'art_service/reports/cumulative_cohort.lock'
|
13
|
+
|
14
|
+
def initialize(start_date:, end_date:, **kwargs)
|
15
|
+
@start_date = ActiveRecord::Base.connection.quote(start_date)
|
16
|
+
@end_date = ActiveRecord::Base.connection.quote(end_date)
|
17
|
+
@rebuild = kwargs[:rebuild]&.casecmp?('true')
|
18
|
+
locations = kwargs[:locations]
|
19
|
+
@locations = locations.present? ? locations.split(',') : []
|
20
|
+
@definition = kwargs[:definition]
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_report
|
24
|
+
start_time = Time.now
|
25
|
+
prepare_tables
|
26
|
+
clear_tables if rebuild
|
27
|
+
process_thread
|
28
|
+
end_time = Time.now
|
29
|
+
time_in_minutes = ((end_time - start_time) / 60).round(2)
|
30
|
+
Rails.logger.info("Cumulative Cohort report took #{time_in_minutes} minutes to generate")
|
31
|
+
{ cohort_time: time_in_minutes }
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# we have these steps to build the cohort report for all patients nation wide
|
37
|
+
# 1. We want to filter out patients who are drug refills and consultations as at the end of the quarter
|
38
|
+
# 2. We want to check who in 1 was registered as facility client at the end of the quarter
|
39
|
+
# 3. We want to the first ARV dispensation date for each patient and may not necessarily be in 2 above but as at the end of the quarter
|
40
|
+
# 4. We want to get information on whether the clients in 3 above have ever registered somewhere else. This is to check if they are transfers in
|
41
|
+
# 5. We want to get information on clients birthdate etc after joining all the above
|
42
|
+
# 6. Finally we want to get the outcomes of the clients in 5 above
|
43
|
+
|
44
|
+
# rubocop:disable Metrics/MethodLength
|
45
|
+
def process_thread
|
46
|
+
# use locations to thread but process 10 locations at a time
|
47
|
+
queue = Queue.new
|
48
|
+
locations.each { |loc| queue << loc }
|
49
|
+
|
50
|
+
threads = Array.new(10) do
|
51
|
+
Thread.new do
|
52
|
+
until queue.empty?
|
53
|
+
loc = begin
|
54
|
+
queue.pop(true)
|
55
|
+
rescue StandardError
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
next unless loc
|
59
|
+
|
60
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
61
|
+
process_data loc
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
threads.each(&:join)
|
68
|
+
end
|
69
|
+
# rubocop:enable Metrics/MethodLength
|
70
|
+
|
71
|
+
def process_data(location)
|
72
|
+
cdr_other_patient_types location
|
73
|
+
external_clients location
|
74
|
+
transfer_ins location
|
75
|
+
min_drug_orders location
|
76
|
+
potential_cohort_members location
|
77
|
+
reason_for_starting_art location
|
78
|
+
cohort_members location
|
79
|
+
outcome = MalawiHivProgramReports::Moh::CumulativeOutcome.new(end_date:, location:, definition:, rebuild:)
|
80
|
+
outcome.find_report
|
81
|
+
end
|
82
|
+
|
83
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
84
|
+
def prepare_tables
|
85
|
+
create_cdr_other_patient_types unless check_if_table_exists('cdr_other_patient_types')
|
86
|
+
create_temp_potential_cohort_members_table unless check_if_table_exists('cdr_temp_potential_cohort_members')
|
87
|
+
create_min_drug_orders_table unless check_if_table_exists('cdr_temp_min_drug_orders')
|
88
|
+
create_transfer_ins_table unless check_if_table_exists('cdr_temp_transfer_ins')
|
89
|
+
create_external_clients_table unless check_if_table_exists('cdr_temp_external_clients')
|
90
|
+
create_temp_cohort_members_table unless check_if_table_exists('cdr_temp_cohort_members')
|
91
|
+
create_cdr_reason_for_starting_art unless check_if_table_exists('cdr_reason_for_starting_art')
|
92
|
+
end
|
93
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
94
|
+
|
95
|
+
def create_external_clients_table
|
96
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
97
|
+
CREATE TABLE IF NOT EXISTS cdr_temp_external_clients (
|
98
|
+
patient_id INT(11) NOT NULL,
|
99
|
+
site_id INT(11) NOT NULL,
|
100
|
+
patient_types VARCHAR(255) DEFAULT NULL,
|
101
|
+
encounter_id INT(11) DEFAULT NULL,
|
102
|
+
PRIMARY KEY (patient_id, site_id)
|
103
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
104
|
+
PARTITION BY LIST(site_id) (#{partition_by_site})
|
105
|
+
SQL
|
106
|
+
end
|
107
|
+
|
108
|
+
def create_min_drug_orders_table
|
109
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
110
|
+
CREATE TABLE IF NOT EXISTS cdr_temp_min_drug_orders (
|
111
|
+
patient_id INT(11) NOT NULL,
|
112
|
+
site_id INT(11) NOT NULL,
|
113
|
+
start_date DATE DEFAULT NULL,
|
114
|
+
PRIMARY KEY (patient_id, site_id)
|
115
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
116
|
+
PARTITION BY LIST(site_id) (#{partition_by_site})
|
117
|
+
SQL
|
118
|
+
end
|
119
|
+
|
120
|
+
def create_transfer_ins_table
|
121
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
122
|
+
CREATE TABLE IF NOT EXISTS cdr_temp_transfer_ins (
|
123
|
+
patient_id INT(11) NOT NULL,
|
124
|
+
site_id INT(11) NOT NULL,
|
125
|
+
value_datetime DATE DEFAULT NULL,
|
126
|
+
PRIMARY KEY (patient_id, site_id)
|
127
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
128
|
+
PARTITION BY LIST(site_id) (#{partition_by_site})
|
129
|
+
SQL
|
130
|
+
end
|
131
|
+
|
132
|
+
# rubocop:disable Metrics/MethodLength
|
133
|
+
def create_temp_potential_cohort_members_table
|
134
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
135
|
+
CREATE TABLE IF NOT EXISTS cdr_temp_potential_cohort_members (
|
136
|
+
patient_id INT(11) NOT NULL,
|
137
|
+
site_id INT(11) NOT NULL,
|
138
|
+
birthdate DATE DEFAULT NULL,
|
139
|
+
birthdate_estimated TINYINT(1) DEFAULT NULL,
|
140
|
+
death_date DATE DEFAULT NULL,
|
141
|
+
gender CHAR(1) DEFAULT NULL,
|
142
|
+
PRIMARY KEY (patient_id, site_id)
|
143
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
144
|
+
PARTITION BY LIST(site_id) (#{partition_by_site})
|
145
|
+
SQL
|
146
|
+
end
|
147
|
+
|
148
|
+
def create_temp_cohort_members_table
|
149
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
150
|
+
CREATE TABLE IF NOT EXISTS cdr_temp_cohort_members (
|
151
|
+
patient_id INT(11) NOT NULL,
|
152
|
+
site_id INT(11) NOT NULL,
|
153
|
+
birthdate DATE DEFAULT NULL,
|
154
|
+
birthdate_estimated TINYINT(1) DEFAULT NULL,
|
155
|
+
death_date DATE DEFAULT NULL,
|
156
|
+
gender CHAR(1) DEFAULT NULL,
|
157
|
+
date_enrolled DATE DEFAULT NULL,
|
158
|
+
earliest_start_date DATE DEFAULT NULL,
|
159
|
+
recorded_start_date DATE DEFAULT NULL,
|
160
|
+
age_at_initiation INT(11) DEFAULT NULL,
|
161
|
+
age_in_days INT(11) DEFAULT NULL,
|
162
|
+
reason_for_starting_art INT(11) DEFAULT NULL,
|
163
|
+
PRIMARY KEY (patient_id, site_id)
|
164
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
165
|
+
PARTITION BY LIST(site_id) (#{partition_by_site})
|
166
|
+
SQL
|
167
|
+
create_cohort_member_indexes
|
168
|
+
end
|
169
|
+
# rubocop:enable Metrics/MethodLength
|
170
|
+
|
171
|
+
def check_if_table_exists(table_name)
|
172
|
+
result = ActiveRecord::Base.connection.select_one <<~SQL
|
173
|
+
SELECT COUNT(*) AS count
|
174
|
+
FROM information_schema.tables
|
175
|
+
WHERE table_schema = DATABASE()
|
176
|
+
AND table_name = '#{table_name}'
|
177
|
+
SQL
|
178
|
+
result['count'].to_i.positive?
|
179
|
+
end
|
180
|
+
|
181
|
+
def create_cohort_member_indexes
|
182
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
183
|
+
CREATE INDEX idx_enrolled ON cdr_temp_cohort_members (date_enrolled)
|
184
|
+
SQL
|
185
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
186
|
+
CREATE INDEX idx_earliest ON cdr_temp_cohort_members (earliest_start_date)
|
187
|
+
SQL
|
188
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
189
|
+
CREATE INDEX idx_recorded ON cdr_temp_cohort_members (recorded_start_date)
|
190
|
+
SQL
|
191
|
+
end
|
192
|
+
|
193
|
+
def create_cdr_other_patient_types
|
194
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
195
|
+
CREATE TABLE IF NOT EXISTS cdr_other_patient_types (
|
196
|
+
patient_id INT(11) NOT NULL,
|
197
|
+
site_id INT(11) NOT NULL,
|
198
|
+
PRIMARY KEY (patient_id, site_id)
|
199
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
200
|
+
PARTITION BY LIST(site_id) (#{partition_by_site})
|
201
|
+
SQL
|
202
|
+
end
|
203
|
+
|
204
|
+
def create_cdr_reason_for_starting_art
|
205
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
206
|
+
CREATE TABLE IF NOT EXISTS cdr_reason_for_starting_art (
|
207
|
+
patient_id INT(11) NOT NULL,
|
208
|
+
site_id INT(11) NOT NULL,
|
209
|
+
reason_for_starting_art INT(11) DEFAULT NULL,
|
210
|
+
PRIMARY KEY (patient_id, site_id)
|
211
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
212
|
+
PARTITION BY LIST(site_id) (#{partition_by_site})
|
213
|
+
SQL
|
214
|
+
create_cdr_reason_for_starting_art_indexes
|
215
|
+
end
|
216
|
+
|
217
|
+
def create_cdr_reason_for_starting_art_indexes
|
218
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
219
|
+
CREATE INDEX idx_reason_for_starting_art ON cdr_reason_for_starting_art (reason_for_starting_art)
|
220
|
+
SQL
|
221
|
+
end
|
222
|
+
|
223
|
+
# rubocop:disable Metrics/MethodLength
|
224
|
+
def cohort_members(location)
|
225
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
226
|
+
INSERT INTO cdr_temp_cohort_members
|
227
|
+
SELECT
|
228
|
+
pcm.patient_id,
|
229
|
+
pcm.site_id,
|
230
|
+
pcm.birthdate,
|
231
|
+
pcm.birthdate_estimated,
|
232
|
+
pcm.death_date,
|
233
|
+
pcm.gender,
|
234
|
+
mdo.start_date AS date_enrolled,
|
235
|
+
COALESCE(ti.value_datetime, mdo.start_date) AS earliest_start_date,
|
236
|
+
ti.value_datetime AS recorded_start_date,
|
237
|
+
IF(pcm.birthdate IS NOT NULL, TIMESTAMPDIFF(YEAR, pcm.birthdate, COALESCE(ti.value_datetime, mdo.start_date)), NULL) AS age_at_initiation,
|
238
|
+
IF(pcm.birthdate IS NOT NULL, TIMESTAMPDIFF(DAY, pcm.birthdate, COALESCE(ti.value_datetime, mdo.start_date)), NULL) AS age_in_days,
|
239
|
+
rfsa.reason_for_starting_art
|
240
|
+
FROM cdr_temp_potential_cohort_members pcm
|
241
|
+
INNER JOIN cdr_temp_min_drug_orders mdo ON mdo.patient_id = pcm.patient_id AND mdo.site_id = pcm.site_id
|
242
|
+
INNER JOIN cdr_reason_for_starting_art rfsa ON rfsa.patient_id = pcm.patient_id AND rfsa.site_id = pcm.site_id
|
243
|
+
LEFT JOIN cdr_temp_transfer_ins ti ON ti.patient_id = pcm.patient_id AND ti.site_id = pcm.site_id
|
244
|
+
WHERE pcm.site_id = #{location}
|
245
|
+
HAVING reason_for_starting_art IS NOT NULL
|
246
|
+
ON DUPLICATE KEY UPDATE birthdate = VALUES(birthdate), birthdate_estimated = VALUES(birthdate_estimated), death_date = VALUES(death_date),
|
247
|
+
gender = VALUES(gender), date_enrolled = VALUES(date_enrolled), earliest_start_date = VALUES(earliest_start_date),
|
248
|
+
recorded_start_date = VALUES(recorded_start_date), age_at_initiation = VALUES(age_at_initiation),
|
249
|
+
age_in_days = VALUES(age_in_days), reason_for_starting_art = VALUES(reason_for_starting_art)
|
250
|
+
SQL
|
251
|
+
end
|
252
|
+
|
253
|
+
def potential_cohort_members(location)
|
254
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
255
|
+
INSERT INTO cdr_temp_potential_cohort_members
|
256
|
+
SELECT
|
257
|
+
pp.patient_id,
|
258
|
+
pp.site_id,
|
259
|
+
p.birthdate,
|
260
|
+
p.birthdate_estimated,
|
261
|
+
p.death_date,
|
262
|
+
LEFT(p.gender, 1) gender
|
263
|
+
FROM patient_program pp
|
264
|
+
INNER JOIN person p ON p.person_id = pp.patient_id AND p.site_id = pp.site_id AND p.voided = 0
|
265
|
+
INNER JOIN patient_state ps ON ps.patient_program_id = pp.patient_program_id AND ps.site_id = pp.site_id
|
266
|
+
AND ps.voided = 0 AND ps.state = 7 AND ps.start_date IS NOT NULL -- 7 is On antiretrovirals
|
267
|
+
AND ps.start_date < (DATE(#{end_date}) + INTERVAL 1 DAY)
|
268
|
+
WHERE pp.program_id = 1 AND pp.voided = 0 AND (pp.patient_id, pp.site_id) NOT IN (SELECT patient_id, site_id FROM cdr_temp_external_clients)
|
269
|
+
AND pp.site_id = #{location}
|
270
|
+
GROUP BY pp.patient_id, pp.site_id
|
271
|
+
ON DUPLICATE KEY UPDATE birthdate = VALUES(birthdate), birthdate_estimated = VALUES(birthdate_estimated), death_date = VALUES(death_date),
|
272
|
+
gender = VALUES(gender)
|
273
|
+
SQL
|
274
|
+
end
|
275
|
+
|
276
|
+
def min_drug_orders(location)
|
277
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
278
|
+
INSERT INTO cdr_temp_min_drug_orders
|
279
|
+
SELECT o.patient_id, o.site_id, DATE(MIN(o.start_date)) start_date
|
280
|
+
FROM orders o
|
281
|
+
INNER JOIN drug_order do ON do.order_id = o.order_id AND do.site_id = o.site_id AND do.quantity > 0
|
282
|
+
LEFT JOIN (
|
283
|
+
SELECT o.person_id patient_id, o.site_id, DATE(MIN(o.obs_datetime)) registered_date
|
284
|
+
FROM obs o
|
285
|
+
INNER JOIN cdr_other_patient_types other ON other.patient_id = o.person_id AND other.site_id = o.site_id
|
286
|
+
WHERE o.concept_id = 3289 -- Type of patient
|
287
|
+
AND o.value_coded = 7572 -- New patient
|
288
|
+
AND o.voided = 0
|
289
|
+
AND o.obs_datetime < (DATE(#{end_date}) + INTERVAL 1 DAY)
|
290
|
+
GROUP BY o.person_id, o.site_id
|
291
|
+
) np ON np.patient_id = o.patient_id AND np.site_id = o.site_id
|
292
|
+
WHERE o.voided = 0 AND o.start_date < (DATE(#{end_date}) + INTERVAL 1 DAY)
|
293
|
+
AND o.start_date > COALESCE(np.registered_date, DATE('1900-01-01'))
|
294
|
+
AND o.concept_id IN (SELECT concept_id FROM concept_set WHERE concept_set = 1085) -- 1085 is ARV DRUGS
|
295
|
+
AND o.order_type_id = 1 AND o.site_id = #{location}
|
296
|
+
GROUP BY o.patient_id, o.site_id
|
297
|
+
ON DUPLICATE KEY UPDATE start_date = VALUES(start_date)
|
298
|
+
SQL
|
299
|
+
end
|
300
|
+
|
301
|
+
def transfer_ins(location)
|
302
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
303
|
+
INSERT INTO cdr_temp_transfer_ins
|
304
|
+
SELECT o.person_id, o.site_id, DATE(MIN(o.value_datetime)) value_datetime
|
305
|
+
FROM obs o
|
306
|
+
INNER JOIN encounter e ON e.patient_id = o.person_id AND e.site_id = o.site_id AND e.encounter_id = o.encounter_id
|
307
|
+
AND e.program_id = 1 AND e.encounter_datetime < (DATE(#{end_date}) + INTERVAL 1 DAY)
|
308
|
+
AND e.encounter_type = 9 -- HIV CLINIC REGISTRATION
|
309
|
+
AND e.voided = 0
|
310
|
+
WHERE o.concept_id = 2516 AND o.voided = 0 AND o.obs_datetime < (DATE(#{end_date}) + INTERVAL 1 DAY) -- 2516 is Date antiretrovirals started
|
311
|
+
AND o.site_id = #{location}
|
312
|
+
GROUP BY o.person_id, o.site_id
|
313
|
+
ON DUPLICATE KEY UPDATE value_datetime = VALUES(value_datetime)
|
314
|
+
SQL
|
315
|
+
end
|
316
|
+
|
317
|
+
def external_clients(location)
|
318
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
319
|
+
INSERT INTO cdr_temp_external_clients
|
320
|
+
SELECT e.patient_id, e.site_id, GROUP_CONCAT(DISTINCT(patient_type.value_coded)) AS patient_types, clinic_registration.encounter_id
|
321
|
+
FROM patient_program as e
|
322
|
+
INNER JOIN obs AS patient_type ON patient_type.person_id = e.patient_id
|
323
|
+
AND patient_type.site_id = e.site_id
|
324
|
+
AND patient_type.voided = 0
|
325
|
+
AND patient_type.concept_id = 3289 -- Type of patient
|
326
|
+
AND patient_type.obs_datetime < DATE(#{end_date}) + INTERVAL 1 DAY
|
327
|
+
LEFT JOIN encounter as clinic_registration ON clinic_registration.patient_id = e.patient_id
|
328
|
+
AND clinic_registration.site_id = e.site_id
|
329
|
+
AND clinic_registration.program_id = 1
|
330
|
+
AND clinic_registration.encounter_type = 9 -- HIV CLINIC REGISTRATION
|
331
|
+
AND clinic_registration.encounter_datetime < DATE(#{end_date}) + INTERVAL 1 DAY
|
332
|
+
AND clinic_registration.voided = 0
|
333
|
+
WHERE e.program_id = 1 -- HIV program
|
334
|
+
AND e.voided = 0
|
335
|
+
AND e.site_id = #{location}
|
336
|
+
-- AND clinic_registration.encounter_id IS NOT NULL -- bone of contention
|
337
|
+
GROUP BY e.patient_id, e.site_id
|
338
|
+
HAVING FIND_IN_SET('7572', patient_types) = 0 AND encounter_id IS NULL
|
339
|
+
ON DUPLICATE KEY UPDATE patient_types = VALUES(patient_types)
|
340
|
+
SQL
|
341
|
+
end
|
342
|
+
|
343
|
+
def cdr_other_patient_types(location)
|
344
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
345
|
+
INSERT INTO cdr_other_patient_types
|
346
|
+
SELECT o.person_id, o.site_id
|
347
|
+
FROM obs o
|
348
|
+
WHERE o.concept_id = 3289 -- Type of patient
|
349
|
+
AND o.value_coded != 7572 -- New patient
|
350
|
+
AND o.voided = 0
|
351
|
+
AND o.obs_datetime < (DATE(#{end_date}) + INTERVAL 1 DAY)
|
352
|
+
AND o.site_id = #{location}
|
353
|
+
GROUP BY o.person_id, o.site_id
|
354
|
+
-- on duplicate just ignore
|
355
|
+
ON DUPLICATE KEY UPDATE site_id = VALUES(site_id)
|
356
|
+
SQL
|
357
|
+
end
|
358
|
+
|
359
|
+
def reason_for_starting_art(location)
|
360
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
361
|
+
INSERT INTO cdr_reason_for_starting_art
|
362
|
+
SELECT a.person_id, a.site_id, a.value_coded
|
363
|
+
FROM obs a
|
364
|
+
INNER JOIN cdr_temp_potential_cohort_members ct ON ct.patient_id = a.person_id AND ct.site_id = a.site_id
|
365
|
+
LEFT OUTER JOIN obs b ON a.person_id = b.person_id AND a.site_id = b.site_id AND b.concept_id = a.concept_id
|
366
|
+
AND b.concept_id = 7563
|
367
|
+
AND a.obs_datetime < b.obs_datetime AND b.voided = 0 AND b.obs_datetime < DATE(#{end_date}) + INTERVAL 1 DAY AND a.obs_datetime < DATE(#{end_date}) + INTERVAL 1 DAY
|
368
|
+
WHERE b.obs_id IS NULL AND a.concept_id = 7563 AND a.voided = 0 AND a.site_id = #{location}
|
369
|
+
GROUP BY a.person_id, a.site_id
|
370
|
+
ON DUPLICATE KEY UPDATE reason_for_starting_art = VALUES(reason_for_starting_art)
|
371
|
+
SQL
|
372
|
+
end
|
373
|
+
|
374
|
+
# rubocop:enable Metrics/MethodLength
|
375
|
+
|
376
|
+
def clear_tables
|
377
|
+
# if locations is empty then we truncating otherwise we clear the locations
|
378
|
+
if locations.empty?
|
379
|
+
ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_other_patient_types')
|
380
|
+
ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_temp_potential_cohort_members')
|
381
|
+
ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_temp_min_drug_orders')
|
382
|
+
ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_temp_transfer_ins')
|
383
|
+
ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_temp_cohort_members')
|
384
|
+
ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_temp_external_clients')
|
385
|
+
ActiveRecord::Base.connection.execute('TRUNCATE TABLE cdr_reason_for_starting_art')
|
386
|
+
else
|
387
|
+
ActiveRecord::Base.connection.execute("DELETE FROM cdr_other_patient_types WHERE site_id IN (#{locations.join(',')})")
|
388
|
+
ActiveRecord::Base.connection.execute("DELETE FROM cdr_temp_potential_cohort_members WHERE site_id IN (#{locations.join(',')})")
|
389
|
+
ActiveRecord::Base.connection.execute("DELETE FROM cdr_temp_min_drug_orders WHERE site_id IN (#{locations.join(',')})")
|
390
|
+
ActiveRecord::Base.connection.execute("DELETE FROM cdr_temp_transfer_ins WHERE site_id IN (#{locations.join(',')})")
|
391
|
+
ActiveRecord::Base.connection.execute("DELETE FROM cdr_temp_cohort_members WHERE site_id IN (#{locations.join(',')})")
|
392
|
+
ActiveRecord::Base.connection.execute("DELETE FROM cdr_temp_external_clients WHERE site_id IN (#{locations.join(',')})")
|
393
|
+
ActiveRecord::Base.connection.execute("DELETE FROM cdr_reason_for_starting_art WHERE site_id IN (#{locations.join(',')})")
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
# rubocop:enable Metrics/ClassLength
|
398
|
+
end
|
399
|
+
end
|
@@ -0,0 +1,545 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MalawiHivProgramReports
|
4
|
+
module Moh
|
5
|
+
# This is the Cumulative Cohort Builder class
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
7
|
+
class CumulativeOutcome
|
8
|
+
include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
|
9
|
+
attr_reader :end_date, :definition, :rebuild, :location
|
10
|
+
|
11
|
+
def initialize(end_date:, definition: 'moh', **kwargs)
|
12
|
+
@end_date = ActiveRecord::Base.connection.quote(end_date.to_date)
|
13
|
+
@definition = definition
|
14
|
+
@rebuild = kwargs[:rebuild]
|
15
|
+
@location = kwargs[:location]
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_report
|
19
|
+
start_time = Time.now
|
20
|
+
prepare_tables
|
21
|
+
clear_tables if rebuild
|
22
|
+
update_steps unless rebuild
|
23
|
+
process_data
|
24
|
+
end_time = Time.now
|
25
|
+
total_time_in_minutes = ((end_time - start_time) / 60).round(2)
|
26
|
+
Rails.logger.info("Cumulative Outcome report completed in #{total_time_in_minutes} minutes")
|
27
|
+
{ cumulative_outcome_time: total_time_in_minutes, definition: }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# The main idea here is to come up with cumulative outcomes for patients in cdr_temp_cohort_members
|
33
|
+
# 1. load_patients_who_died
|
34
|
+
# 2. load_patients_who_stopped_treatment
|
35
|
+
# 3. load_patients_on_pre_art
|
36
|
+
# 4. load_patients_without_state
|
37
|
+
# 5. load_patients_without_drug_orders
|
38
|
+
# 6. load_patients_on_treatment
|
39
|
+
# 7. load_defaulters
|
40
|
+
|
41
|
+
def program_states(*names)
|
42
|
+
::ProgramWorkflowState.joins(:program_workflow)
|
43
|
+
.joins(:concept)
|
44
|
+
.merge(::ProgramWorkflow.where(program: ::Program.find_by_name('HIV Program')))
|
45
|
+
.merge(::Concept.joins(:concept_names)
|
46
|
+
.merge(::ConceptName.where(name: names)))
|
47
|
+
.select(:program_workflow_state_id)
|
48
|
+
end
|
49
|
+
|
50
|
+
# ===================================
|
51
|
+
# Data Management Region
|
52
|
+
# ===================================
|
53
|
+
# rubocop:disable Metrics/MethodLength
|
54
|
+
def process_data
|
55
|
+
load_max_drug_orders
|
56
|
+
load_min_auto_expire_date
|
57
|
+
load_max_patient_state
|
58
|
+
load_max_appointment_date
|
59
|
+
# HIC SUNT DRACONIS: The order of the operations below matters,
|
60
|
+
# do not change it unless you know what you are doing!!!
|
61
|
+
load_patients_who_died
|
62
|
+
load_patients_who_stopped_treatment
|
63
|
+
load_patients_on_pre_art
|
64
|
+
load_patients_without_state
|
65
|
+
load_patients_without_drug_orders
|
66
|
+
load_patients_on_treatment
|
67
|
+
load_without_clinical_contact
|
68
|
+
load_defaulters
|
69
|
+
end
|
70
|
+
|
71
|
+
def load_max_drug_orders
|
72
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
73
|
+
INSERT INTO cdr_temp_max_drug_orders
|
74
|
+
SELECT o.patient_id, o.site_id, MAX(o.start_date) AS start_date
|
75
|
+
FROM orders o
|
76
|
+
INNER JOIN cdr_temp_cohort_members tesd ON tesd.patient_id = o.patient_id AND tesd.site_id = o.site_id
|
77
|
+
INNER JOIN drug_order ON drug_order.order_id = o.order_id
|
78
|
+
AND drug_order.site_id = o.site_id AND drug_order.quantity > 0
|
79
|
+
AND drug_order.drug_inventory_id IN (#{arv_drug})
|
80
|
+
WHERE o.order_type_id = 1 -- drug order
|
81
|
+
AND o.start_date < (DATE(#{end_date}) + INTERVAL 1 DAY)
|
82
|
+
AND o.voided = 0 AND o.site_id = #{location}
|
83
|
+
GROUP BY o.patient_id, o.site_id
|
84
|
+
ON DUPLICATE KEY UPDATE start_date = VALUES(start_date)
|
85
|
+
SQL
|
86
|
+
end
|
87
|
+
|
88
|
+
def load_min_auto_expire_date
|
89
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
90
|
+
INSERT INTO cdr_temp_min_auto_expire_date
|
91
|
+
SELECT patient_id, o.site_id, MIN(auto_expire_date) AS auto_expire_date
|
92
|
+
FROM orders o
|
93
|
+
INNER JOIN cdr_temp_max_drug_orders USING (patient_id, site_id, start_date)
|
94
|
+
INNER JOIN drug_order ON drug_order.order_id = o.order_id
|
95
|
+
AND drug_order.site_id = o.site_id AND drug_order.quantity > 0
|
96
|
+
AND drug_order.drug_inventory_id IN (#{arv_drug})
|
97
|
+
WHERE o.order_type_id = 1
|
98
|
+
AND o.voided = 0 AND o.site_id = #{location}
|
99
|
+
GROUP BY patient_id, o.site_id
|
100
|
+
HAVING auto_expire_date IS NOT NULL
|
101
|
+
ON DUPLICATE KEY UPDATE auto_expire_date = VALUES(auto_expire_date)
|
102
|
+
SQL
|
103
|
+
end
|
104
|
+
|
105
|
+
def load_max_appointment_date
|
106
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
107
|
+
INSERT INTO cdr_temp_max_patient_appointment
|
108
|
+
SELECT o.person_id, o.site_id, DATE(MAX(o.value_datetime)) appointment_date
|
109
|
+
FROM obs o
|
110
|
+
INNER JOIN encounter e ON e.encounter_id = o.encounter_id AND e.site_id = o.site_id AND e.voided = 0
|
111
|
+
AND e.program_id = 1 AND e.encounter_datetime < DATE(#{end_date}) + INTERVAL 1 DAY
|
112
|
+
WHERE o.concept_id = 5096 AND o.voided = 0 AND o.obs_datetime < DATE(#{end_date}) + INTERVAL 1 DAY AND o.site_id = #{location}
|
113
|
+
GROUP BY o.person_id, o.site_id
|
114
|
+
HAVING appointment_date IS NOT NULL
|
115
|
+
ON DUPLICATE KEY UPDATE appointment_date = VALUES(appointment_date)
|
116
|
+
SQL
|
117
|
+
end
|
118
|
+
|
119
|
+
def load_max_patient_state
|
120
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
121
|
+
INSERT INTO cdr_temp_max_patient_state
|
122
|
+
SELECT pp.patient_id, pp.site_id, MAX(ps.start_date) start_date
|
123
|
+
FROM patient_state ps
|
124
|
+
INNER JOIN patient_program pp ON pp.patient_program_id = ps.patient_program_id AND pp.site_id = ps.site_id AND pp.program_id = 1 AND pp.voided = 0
|
125
|
+
WHERE ps.start_date < DATE(#{end_date}) + INTERVAL 1 DAY
|
126
|
+
AND ps.voided = 0 AND ps.site_id = #{location}
|
127
|
+
GROUP BY pp.patient_id, pp.site_id
|
128
|
+
HAVING start_date IS NOT NULL
|
129
|
+
ON DUPLICATE KEY UPDATE start_date = VALUES(start_date)
|
130
|
+
SQL
|
131
|
+
end
|
132
|
+
|
133
|
+
# Loads all patiens with an outcome of died as of given date
|
134
|
+
# into the temp_patient_outcomes table.
|
135
|
+
def load_patients_who_died
|
136
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
137
|
+
INSERT INTO cdr_temp_patient_outcomes
|
138
|
+
SELECT patients.patient_id, 'Patient died', patient_state.start_date, patients.site_id, 1
|
139
|
+
FROM cdr_temp_cohort_members AS patients
|
140
|
+
INNER JOIN patient_program
|
141
|
+
ON patient_program.patient_id = patients.patient_id
|
142
|
+
AND patient_program.site_id = patients.site_id
|
143
|
+
AND patient_program.program_id = 1
|
144
|
+
AND patient_program.voided = 0
|
145
|
+
INNER JOIN patient_state
|
146
|
+
ON patient_state.patient_program_id = patient_program.patient_program_id
|
147
|
+
AND patient_state.site_id = patient_program.site_id
|
148
|
+
AND patient_state.state = (#{program_states('Patient died').limit(1).to_sql})
|
149
|
+
AND patient_state.start_date < DATE(#{end_date}) + INTERVAL 1 DAY
|
150
|
+
AND patient_state.voided = 0
|
151
|
+
WHERE patients.date_enrolled <= DATE(#{end_date}) AND patients.site_id = #{location}
|
152
|
+
AND patient_state.date_created = (
|
153
|
+
SELECT MAX(date_created)
|
154
|
+
FROM patient_state ps
|
155
|
+
WHERE ps.patient_program_id = patient_state.patient_program_id AND ps.site_id = patient_state.site_id
|
156
|
+
AND ps.state = patient_state.state AND ps.voided = 0 AND ps.start_date <= #{end_date})
|
157
|
+
GROUP BY patients.patient_id, patients.site_id
|
158
|
+
ON DUPLICATE KEY UPDATE cum_outcome = VALUES(cum_outcome), outcome_date = VALUES(outcome_date), step = VALUES(step)
|
159
|
+
SQL
|
160
|
+
end
|
161
|
+
|
162
|
+
# Loads all patients with an outcome of transferred out or
|
163
|
+
# treatment stopped into temp_patient_outcomes table.
|
164
|
+
def load_patients_who_stopped_treatment
|
165
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
166
|
+
INSERT INTO cdr_temp_patient_outcomes
|
167
|
+
SELECT patients.patient_id,
|
168
|
+
(
|
169
|
+
SELECT name FROM concept_name
|
170
|
+
WHERE concept_id = (
|
171
|
+
SELECT concept_id FROM program_workflow_state
|
172
|
+
WHERE program_workflow_state_id = patient_state.state
|
173
|
+
LIMIT 1
|
174
|
+
)
|
175
|
+
) AS cum_outcome,
|
176
|
+
patient_state.start_date, patients.site_id, 2
|
177
|
+
FROM cdr_temp_cohort_members AS patients
|
178
|
+
INNER JOIN patient_program
|
179
|
+
ON patient_program.patient_id = patients.patient_id
|
180
|
+
AND patient_program.site_id = patients.site_id
|
181
|
+
AND patient_program.program_id = 1
|
182
|
+
AND patient_program.voided = 0
|
183
|
+
INNER JOIN patient_state
|
184
|
+
ON patient_state.patient_program_id = patient_program.patient_program_id
|
185
|
+
AND patient_state.site_id = patient_program.site_id
|
186
|
+
AND patient_state.state IN (#{program_states('Patient transferred out', 'Treatment stopped').to_sql})
|
187
|
+
AND patient_state.start_date < DATE(#{end_date}) + INTERVAL 1 DAY
|
188
|
+
AND (patient_state.end_date >= #{end_date} OR patient_state.end_date IS NULL)
|
189
|
+
AND patient_state.voided = 0
|
190
|
+
INNER JOIN cdr_temp_max_patient_state AS max_patient_state
|
191
|
+
ON max_patient_state.patient_id = patient_program.patient_id
|
192
|
+
AND max_patient_state.site_id = patient_state.site_id
|
193
|
+
AND max_patient_state.start_date = patient_state.start_date
|
194
|
+
WHERE patients.date_enrolled <= #{end_date} AND patients.site_id = #{location}
|
195
|
+
AND (patients.patient_id, patients.site_id) NOT IN (SELECT patient_id, site_id FROM cdr_temp_patient_outcomes WHERE step = 1)
|
196
|
+
GROUP BY patients.patient_id, patients.site_id
|
197
|
+
ON DUPLICATE KEY UPDATE cum_outcome = VALUES(cum_outcome), outcome_date = VALUES(outcome_date), step = VALUES(step)
|
198
|
+
SQL
|
199
|
+
end
|
200
|
+
|
201
|
+
# Load all patients on Pre-ART.
|
202
|
+
def load_patients_on_pre_art
|
203
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
204
|
+
INSERT INTO cdr_temp_patient_outcomes
|
205
|
+
SELECT patients.patient_id,
|
206
|
+
CASE
|
207
|
+
WHEN #{current_defaulter_function('patients.patient_id', 'patients.site_id')} = 1 THEN 'Defaulted'
|
208
|
+
ELSE 'Pre-ART (Continue)'
|
209
|
+
END AS cum_outcome,
|
210
|
+
patient_state.start_date, patients.site_id, 3
|
211
|
+
FROM cdr_temp_cohort_members AS patients
|
212
|
+
INNER JOIN patient_program
|
213
|
+
ON patient_program.patient_id = patients.patient_id
|
214
|
+
AND patient_program.site_id = patients.site_id
|
215
|
+
AND patient_program.program_id = 1
|
216
|
+
AND patient_program.voided = 0
|
217
|
+
INNER JOIN patient_state
|
218
|
+
ON patient_state.patient_program_id = patient_program.patient_program_id
|
219
|
+
AND patient_state.site_id = patient_program.site_id
|
220
|
+
AND patient_state.state = (#{program_states('Pre-ART (Continue)').limit(1).to_sql})
|
221
|
+
AND patient_state.start_date < DATE(#{end_date}) + INTERVAL 1 DAY
|
222
|
+
AND (patient_state.end_date >= #{end_date} OR patient_state.end_date IS NULL)
|
223
|
+
AND patient_state.voided = 0
|
224
|
+
INNER JOIN cdr_temp_max_patient_state AS max_patient_state
|
225
|
+
ON max_patient_state.patient_id = patient_program.patient_id
|
226
|
+
AND max_patient_state.site_id = patient_state.site_id
|
227
|
+
AND max_patient_state.start_date = patient_state.start_date
|
228
|
+
WHERE patients.date_enrolled <= #{end_date} AND patients.site_id = #{location}
|
229
|
+
AND (patients.patient_id, patients.site_id) NOT IN (SELECT patient_id, site_id FROM cdr_temp_patient_outcomes WHERE step IN (1, 2))
|
230
|
+
GROUP BY patients.patient_id, patients.site_id
|
231
|
+
ON DUPLICATE KEY UPDATE cum_outcome = VALUES(cum_outcome), outcome_date = VALUES(outcome_date), step = VALUES(step)
|
232
|
+
SQL
|
233
|
+
end
|
234
|
+
|
235
|
+
# Load all patients without a state
|
236
|
+
def load_patients_without_state
|
237
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
238
|
+
INSERT INTO cdr_temp_patient_outcomes
|
239
|
+
SELECT patients.patient_id,
|
240
|
+
CASE
|
241
|
+
WHEN #{current_defaulter_function('patients.patient_id', 'patients.site_id')} = 1 THEN 'Defaulted'
|
242
|
+
ELSE 'Unknown'
|
243
|
+
END AS cum_outcome,
|
244
|
+
NULL, patients.site_id, 4
|
245
|
+
FROM cdr_temp_cohort_members AS patients
|
246
|
+
INNER JOIN patient_program
|
247
|
+
ON patient_program.patient_id = patients.patient_id
|
248
|
+
AND patient_program.site_id = patients.site_id
|
249
|
+
AND patient_program.program_id = 1
|
250
|
+
AND patient_program.voided = 0
|
251
|
+
WHERE patients.date_enrolled <= #{end_date} AND patients.site_id = #{location}
|
252
|
+
AND (patient_program.patient_program_id, patient_program.site_id) NOT IN (
|
253
|
+
SELECT patient_program_id, site_id
|
254
|
+
FROM patient_state
|
255
|
+
WHERE start_date < DATE(#{end_date}) + INTERVAL 1 DAY AND voided = 0
|
256
|
+
)
|
257
|
+
AND (patients.patient_id, patients.site_id) NOT IN (SELECT patient_id, site_id FROM cdr_temp_patient_outcomes WHERE step IN (1, 2, 3))
|
258
|
+
GROUP BY patients.patient_id, patients.site_id
|
259
|
+
HAVING cum_outcome = 'Defaulted'
|
260
|
+
ON DUPLICATE KEY UPDATE cum_outcome = VALUES(cum_outcome), outcome_date = VALUES(outcome_date), step = VALUES(step)
|
261
|
+
SQL
|
262
|
+
end
|
263
|
+
|
264
|
+
# Load all patients without drug orders or have drug orders
|
265
|
+
# without a quantity.
|
266
|
+
def load_patients_without_drug_orders
|
267
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
268
|
+
INSERT INTO cdr_temp_patient_outcomes
|
269
|
+
SELECT patients.patient_id,
|
270
|
+
'Unknown',
|
271
|
+
NULL, patients.site_id, 5
|
272
|
+
FROM cdr_temp_cohort_members AS patients
|
273
|
+
WHERE date_enrolled <= #{end_date} AND site_id = #{location}
|
274
|
+
AND (patient_id, site_id) NOT IN (SELECT patient_id, site_id FROM cdr_temp_patient_outcomes WHERE step IN (1, 2, 3, 4))
|
275
|
+
AND (patient_id, site_id) NOT IN (SELECT patient_id, site_id FROM cdr_temp_max_drug_orders)
|
276
|
+
ON DUPLICATE KEY UPDATE cum_outcome = VALUES(cum_outcome), outcome_date = VALUES(outcome_date), step = VALUES(step)
|
277
|
+
SQL
|
278
|
+
end
|
279
|
+
|
280
|
+
# Loads all patients who are on treatment
|
281
|
+
def load_patients_on_treatment
|
282
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
283
|
+
INSERT INTO cdr_temp_patient_outcomes
|
284
|
+
SELECT patients.patient_id, 'On antiretrovirals', patient_state.start_date, patients.site_id, 6
|
285
|
+
FROM cdr_temp_cohort_members AS patients
|
286
|
+
INNER JOIN patient_program
|
287
|
+
ON patient_program.patient_id = patients.patient_id
|
288
|
+
AND patient_program.site_id = patients.site_id
|
289
|
+
AND patient_program.program_id = 1
|
290
|
+
AND patient_program.voided = 0
|
291
|
+
/* Get patients' `on ARV` states that are before given date */
|
292
|
+
INNER JOIN patient_state
|
293
|
+
ON patient_state.patient_program_id = patient_program.patient_program_id
|
294
|
+
AND patient_state.site_id = patient_program.site_id
|
295
|
+
AND patient_state.state = 7 -- ON ART
|
296
|
+
AND patient_state.start_date < DATE(#{end_date}) + INTERVAL 1 DAY
|
297
|
+
AND (patient_state.end_date >= #{end_date} OR patient_state.end_date IS NULL)
|
298
|
+
AND patient_state.voided = 0
|
299
|
+
/* Select only the most recent state out of those retrieved above */
|
300
|
+
INNER JOIN cdr_temp_max_patient_state AS max_patient_state
|
301
|
+
ON max_patient_state.patient_id = patient_program.patient_id
|
302
|
+
AND max_patient_state.site_id = patient_state.site_id
|
303
|
+
AND max_patient_state.start_date = patient_state.start_date
|
304
|
+
/* HACK: Ensure that the states captured above do correspond have corresponding
|
305
|
+
ARV dispensations. In other words filter out any `on ARVs` states whose
|
306
|
+
dispensation's may have been voided or states that were created manually
|
307
|
+
without any drugs being dispensed. */
|
308
|
+
INNER JOIN cdr_temp_min_auto_expire_date AS first_order_to_expire
|
309
|
+
ON first_order_to_expire.patient_id = patient_program.patient_id
|
310
|
+
AND first_order_to_expire.site_id = patient_program.site_id
|
311
|
+
AND (first_order_to_expire.auto_expire_date >= #{end_date} OR TIMESTAMPDIFF(DAY,first_order_to_expire.auto_expire_date, #{end_date}) <= #{@definition == 'pepfar' ? 28 : 56})
|
312
|
+
WHERE patients.date_enrolled <= #{end_date} AND patients.site_id = #{location}
|
313
|
+
AND (patients.patient_id, patients.site_id) NOT IN (SELECT patient_id, site_id FROM cdr_temp_patient_outcomes WHERE step IN (1, 2, 3, 4, 5))
|
314
|
+
GROUP BY patients.patient_id, patients.site_id
|
315
|
+
ON DUPLICATE KEY UPDATE cum_outcome = VALUES(cum_outcome), outcome_date = VALUES(outcome_date), step = VALUES(step)
|
316
|
+
SQL
|
317
|
+
end
|
318
|
+
|
319
|
+
def load_without_clinical_contact
|
320
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
321
|
+
INSERT INTO cdr_temp_patient_outcomes
|
322
|
+
SELECT patients.patient_id, 'Defaulted', null, patients.site_id, 7
|
323
|
+
FROM cdr_temp_cohort_members AS patients
|
324
|
+
INNER JOIN patient_program
|
325
|
+
ON patient_program.patient_id = patients.patient_id
|
326
|
+
AND patient_program.site_id = patients.site_id
|
327
|
+
AND patient_program.program_id = 1
|
328
|
+
AND patient_program.voided = 0
|
329
|
+
/* Get patients' `on ARV` states that are before given date */
|
330
|
+
INNER JOIN patient_state
|
331
|
+
ON patient_state.patient_program_id = patient_program.patient_program_id
|
332
|
+
AND patient_state.site_id = patient_program.site_id
|
333
|
+
AND patient_state.state = 7 -- On ART
|
334
|
+
AND patient_state.start_date < DATE(#{end_date}) + INTERVAL 1 DAY
|
335
|
+
AND (patient_state.end_date >= #{end_date} OR patient_state.end_date IS NULL)
|
336
|
+
AND patient_state.voided = 0
|
337
|
+
/* Select only the most recent state out of those retrieved above */
|
338
|
+
INNER JOIN cdr_temp_max_patient_state AS max_patient_state
|
339
|
+
ON max_patient_state.patient_id = patient_program.patient_id
|
340
|
+
AND max_patient_state.site_id = patient_state.site_id
|
341
|
+
AND max_patient_state.start_date = patient_state.start_date
|
342
|
+
INNER JOIN cdr_temp_max_patient_appointment app ON app.patient_id = patients.patient_id
|
343
|
+
AND app.site_id = patients.site_id AND app.appointment_date < #{end_date}
|
344
|
+
INNER JOIN cdr_temp_min_auto_expire_date AS first_order_to_expire
|
345
|
+
ON first_order_to_expire.patient_id = patient_program.patient_id
|
346
|
+
AND first_order_to_expire.site_id = patient_program.site_id
|
347
|
+
AND TIMESTAMPDIFF(DAY,app.appointment_date, first_order_to_expire.auto_expire_date) >= 0
|
348
|
+
AND TIMESTAMPDIFF(DAY,app.appointment_date, first_order_to_expire.auto_expire_date) <= 5
|
349
|
+
AND first_order_to_expire.auto_expire_date < #{end_date}
|
350
|
+
AND TIMESTAMPDIFF(DAY,first_order_to_expire.auto_expire_date, #{end_date}) >= 365
|
351
|
+
WHERE patients.date_enrolled <= #{end_date} AND patients.site_id = #{location}
|
352
|
+
AND (patients.patient_id, patients.site_id) NOT IN (SELECT patient_id, site_id FROM cdr_temp_patient_outcomes WHERE step IN (1, 2, 3, 4, 5, 6))
|
353
|
+
GROUP BY patients.patient_id, patients.site_id
|
354
|
+
ON DUPLICATE KEY UPDATE cum_outcome = VALUES(cum_outcome), outcome_date = VALUES(outcome_date), step = VALUES(step)
|
355
|
+
SQL
|
356
|
+
end
|
357
|
+
|
358
|
+
# Load defaulters
|
359
|
+
def load_defaulters
|
360
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
361
|
+
INSERT INTO cdr_temp_patient_outcomes
|
362
|
+
SELECT patient_id, #{patient_outcome_function('patient_id', 'site_id')}, NULL, site_id, 8
|
363
|
+
FROM cdr_temp_cohort_members
|
364
|
+
WHERE date_enrolled <= #{end_date} AND site_id = #{location}
|
365
|
+
AND (patient_id, site_id) NOT IN (SELECT patient_id, site_id FROM cdr_temp_patient_outcomes WHERE step IN (1, 2, 3, 4, 5, 6, 7))
|
366
|
+
ON DUPLICATE KEY UPDATE cum_outcome = VALUES(cum_outcome), outcome_date = VALUES(outcome_date), step = VALUES(step)
|
367
|
+
SQL
|
368
|
+
end
|
369
|
+
|
370
|
+
# rubocop:enable Metrics/MethodLength
|
371
|
+
|
372
|
+
# ===================================
|
373
|
+
# Function Management Region
|
374
|
+
# ===================================
|
375
|
+
|
376
|
+
def current_defaulter_function(sql_column, site_id)
|
377
|
+
case definition
|
378
|
+
when 'moh' then "current_defaulter(#{sql_column}, #{end_date}, #{site_id})"
|
379
|
+
when 'pepfar' then "current_pepfar_defaulter(#{sql_column}, #{end_date}, #{site_id})"
|
380
|
+
else raise "Invalid outcomes definition: #{definition}" # Should never happen but you never know!
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def patient_outcome_function(sql_column, site_id)
|
385
|
+
case definition
|
386
|
+
when 'moh' then "patient_outcome(#{sql_column}, #{end_date}, #{site_id})"
|
387
|
+
when 'pepfar' then "pepfar_patient_outcome(#{sql_column}, #{end_date}, #{site_id})"
|
388
|
+
else raise "Invalid outcomes definition: #{definition}"
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# ===================================
|
393
|
+
# Table Management Region
|
394
|
+
# ===================================
|
395
|
+
def prepare_tables
|
396
|
+
create_outcome_table unless check_if_table_exists('cdr_temp_patient_outcomes')
|
397
|
+
create_tmp_max_drug_orders_table unless check_if_table_exists('cdr_temp_max_drug_orders')
|
398
|
+
create_tmp_min_auto_expire_date unless check_if_table_exists('cdr_temp_min_auto_expire_date')
|
399
|
+
create_cdr_temp_max_patient_state unless check_if_table_exists('cdr_temp_max_patient_state')
|
400
|
+
create_max_patient_appointment_date unless check_if_table_exists('cdr_temp_max_patient_appointment')
|
401
|
+
end
|
402
|
+
|
403
|
+
def check_if_table_exists(table_name)
|
404
|
+
result = ActiveRecord::Base.connection.select_one <<~SQL
|
405
|
+
SELECT COUNT(*) AS count
|
406
|
+
FROM information_schema.tables
|
407
|
+
WHERE table_schema = DATABASE()
|
408
|
+
AND table_name = '#{table_name}'
|
409
|
+
SQL
|
410
|
+
result['count'].to_i.positive?
|
411
|
+
end
|
412
|
+
|
413
|
+
def create_outcome_table
|
414
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
415
|
+
CREATE TABLE IF NOT EXISTS cdr_temp_patient_outcomes (
|
416
|
+
patient_id INT NOT NULL,
|
417
|
+
cum_outcome VARCHAR(120) NOT NULL,
|
418
|
+
outcome_date DATE DEFAULT NULL,
|
419
|
+
site_id INT NOT NULL,
|
420
|
+
step INT DEFAULT 0,
|
421
|
+
PRIMARY KEY (patient_id, site_id)
|
422
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
423
|
+
PARTITION BY LIST(site_id) (#{partition_by_site})
|
424
|
+
SQL
|
425
|
+
create_outcome_indexes
|
426
|
+
end
|
427
|
+
|
428
|
+
def create_outcome_indexes
|
429
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
430
|
+
CREATE INDEX idx_outcome ON cdr_temp_patient_outcomes (cum_outcome)
|
431
|
+
SQL
|
432
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
433
|
+
CREATE INDEX idx_out_date ON cdr_temp_patient_outcomes (outcome_date)
|
434
|
+
SQL
|
435
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
436
|
+
CREATE INDEX idx_out_step ON cdr_temp_patient_outcomes (step)
|
437
|
+
SQL
|
438
|
+
end
|
439
|
+
|
440
|
+
def create_tmp_max_drug_orders_table
|
441
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
442
|
+
CREATE TABLE IF NOT EXISTS cdr_temp_max_drug_orders (
|
443
|
+
patient_id INT NOT NULL,
|
444
|
+
site_id INT NOT NULL,
|
445
|
+
start_date DATETIME DEFAULT NULL,
|
446
|
+
PRIMARY KEY (patient_id, site_id)
|
447
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
448
|
+
PARTITION BY LIST(site_id) (#{partition_by_site})
|
449
|
+
SQL
|
450
|
+
create_max_drug_orders_indexes
|
451
|
+
end
|
452
|
+
|
453
|
+
def create_max_drug_orders_indexes
|
454
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
455
|
+
CREATE INDEX idx_max_orders ON cdr_temp_max_drug_orders (start_date)
|
456
|
+
SQL
|
457
|
+
end
|
458
|
+
|
459
|
+
def create_tmp_min_auto_expire_date
|
460
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
461
|
+
CREATE TABLE IF NOT EXISTS cdr_temp_min_auto_expire_date (
|
462
|
+
patient_id INT NOT NULL,
|
463
|
+
site_id INT NOT NULL,
|
464
|
+
auto_expire_date DATE DEFAULT NULL,
|
465
|
+
PRIMARY KEY (patient_id, site_id)
|
466
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
467
|
+
PARTITION BY LIST(site_id) (#{partition_by_site})
|
468
|
+
SQL
|
469
|
+
create_min_auto_expire_date_indexes
|
470
|
+
end
|
471
|
+
|
472
|
+
def create_min_auto_expire_date_indexes
|
473
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
474
|
+
CREATE INDEX idx_min_auto_expire_date ON cdr_temp_min_auto_expire_date (auto_expire_date)
|
475
|
+
SQL
|
476
|
+
end
|
477
|
+
|
478
|
+
def create_cdr_temp_max_patient_state
|
479
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
480
|
+
CREATE TABLE IF NOT EXISTS cdr_temp_max_patient_state (
|
481
|
+
patient_id INT NOT NULL,
|
482
|
+
site_id INT NOT NULL,
|
483
|
+
start_date VARCHAR(15) DEFAULT NULL,
|
484
|
+
PRIMARY KEY (patient_id, site_id)
|
485
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
486
|
+
PARTITION BY LIST(site_id) (#{partition_by_site})
|
487
|
+
SQL
|
488
|
+
create_max_patient_state_indexes
|
489
|
+
end
|
490
|
+
|
491
|
+
def create_max_patient_state_indexes
|
492
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
493
|
+
CREATE INDEX idx_max_patient_state ON cdr_temp_max_patient_state (start_date)
|
494
|
+
SQL
|
495
|
+
end
|
496
|
+
|
497
|
+
def create_max_patient_appointment_date
|
498
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
499
|
+
CREATE TABLE IF NOT EXISTS cdr_temp_max_patient_appointment (
|
500
|
+
patient_id INT NOT NULL,
|
501
|
+
site_id INT NOT NULL,
|
502
|
+
appointment_date DATE NOT NULL,
|
503
|
+
PRIMARY KEY (patient_id, site_id)
|
504
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
505
|
+
PARTITION BY LIST(site_id) (#{partition_by_site})
|
506
|
+
SQL
|
507
|
+
create_max_patient_appointment_date_indexes
|
508
|
+
end
|
509
|
+
|
510
|
+
def create_max_patient_appointment_date_indexes
|
511
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
512
|
+
CREATE INDEX idx_max_patient_appointment_date ON cdr_temp_max_patient_appointment (appointment_date)
|
513
|
+
SQL
|
514
|
+
end
|
515
|
+
|
516
|
+
def update_steps
|
517
|
+
ActiveRecord::Base.connection.execute <<~SQL
|
518
|
+
UPDATE cdr_temp_patient_outcomes SET step = 0 WHERE step > 0 AND site_id = #{location}
|
519
|
+
SQL
|
520
|
+
end
|
521
|
+
|
522
|
+
def arv_drug
|
523
|
+
@arv_drug ||= ::Drug.arv_drugs.map(&:drug_id).join(',')
|
524
|
+
end
|
525
|
+
|
526
|
+
def clear_tables
|
527
|
+
# we truncate if @locations is empty otherwise we only clean the specifieds
|
528
|
+
if location.empty?
|
529
|
+
ActiveRecord::Base.connection.execute('TRUNCATE cdr_temp_patient_outcomes')
|
530
|
+
ActiveRecord::Base.connection.execute('TRUNCATE cdr_temp_max_drug_orders')
|
531
|
+
ActiveRecord::Base.connection.execute('TRUNCATE cdr_temp_min_auto_expire_date')
|
532
|
+
ActiveRecord::Base.connection.execute('TRUNCATE cdr_temp_max_patient_state')
|
533
|
+
ActiveRecord::Base.connection.execute('TRUNCATE cdr_temp_max_patient_appointment')
|
534
|
+
else
|
535
|
+
ActiveRecord::Base.connection.execute("DELETE FROM cdr_temp_patient_outcomes WHERE site_id = #{location}")
|
536
|
+
ActiveRecord::Base.connection.execute("DELETE FROM cdr_temp_max_drug_orders WHERE site_id = #{location}")
|
537
|
+
ActiveRecord::Base.connection.execute("DELETE FROM cdr_temp_min_auto_expire_date WHERE site_id = #{location}")
|
538
|
+
ActiveRecord::Base.connection.execute("DELETE FROM cdr_temp_max_patient_state WHERE site_id = #{location}")
|
539
|
+
ActiveRecord::Base.connection.execute("DELETE FROM cdr_temp_max_patient_appointment WHERE site_id = #{location}")
|
540
|
+
end
|
541
|
+
end
|
542
|
+
end
|
543
|
+
# rubocop:enable Metrics/ClassLength
|
544
|
+
end
|
545
|
+
end
|
@@ -6,7 +6,7 @@ module MalawiHivProgramReports
|
|
6
6
|
'ARCHIVING_CANDIDATES' => MalawiHivProgramReports::ArchivingCandidates,
|
7
7
|
'APPOINTMENTS' => MalawiHivProgramReports::Clinic::AppointmentsReport,
|
8
8
|
'ARV_REFILL_PERIODS' => MalawiHivProgramReports::ArvRefillPeriods,
|
9
|
-
'COHORT' => MalawiHivProgramReports::Moh::
|
9
|
+
'COHORT' => MalawiHivProgramReports::Moh::ArtCohort,
|
10
10
|
'COHORT_DISAGGREGATED' => MalawiHivProgramReports::Moh::CohortDisaggregated,
|
11
11
|
'COHORT_DISAGGREGATED_ADDITIONS' => MalawiHivProgramReports::Moh::CohortDisaggregatedAdditions,
|
12
12
|
'COHORT_SURVIVAL_ANALYSIS' => MalawiHivProgramReports::Moh::CohortSurvivalAnalysis,
|
@@ -50,7 +50,9 @@ module MalawiHivProgramReports
|
|
50
50
|
'DISCREPANCY_REPORT' => MalawiHivProgramReports::Clinic::DiscrepancyReport,
|
51
51
|
'STOCK_CARD' => MalawiHivProgramReports::Clinic::StockCardReport,
|
52
52
|
'HYPERTENSION_REPORT' => MalawiHivProgramReports::Clinic::HypertensionReport,
|
53
|
-
'TX_NEW' => MalawiHivProgramReports::Pepfar::TxNew
|
53
|
+
'TX_NEW' => MalawiHivProgramReports::Pepfar::TxNew,
|
54
|
+
'CUMULATIVE_COHORT' => MalawiHivProgramReports::Moh::CumulativeCohort,
|
55
|
+
'CUMULATIVE_OUTCOME' => MalawiHivProgramReports::Moh::CumulativeOutcome
|
54
56
|
}.freeze
|
55
57
|
end
|
56
58
|
end
|
@@ -55,6 +55,12 @@ module MalawiHivProgramReports
|
|
55
55
|
#{site_manager(operator: 'AND', column: 'a.site_id', location: @location)}
|
56
56
|
SQL
|
57
57
|
end
|
58
|
+
|
59
|
+
def partition_by_site
|
60
|
+
@partition_by_site ||= Location.all.map do |location|
|
61
|
+
"PARTITION p#{location.id} VALUES IN (#{location.id}) ENGINE = InnoDB"
|
62
|
+
end.join(', ')
|
63
|
+
end
|
58
64
|
end
|
59
65
|
end
|
60
66
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: malawi_hiv_program_reports
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.17
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roy Chanunkha
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-04-
|
11
|
+
date: 2024-04-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -129,13 +129,15 @@ files:
|
|
129
129
|
- app/services/malawi_hiv_program_reports/cohort/regimens.rb
|
130
130
|
- app/services/malawi_hiv_program_reports/cohort/side_effects.rb
|
131
131
|
- app/services/malawi_hiv_program_reports/cohort/tpt.rb
|
132
|
-
- app/services/malawi_hiv_program_reports/moh/
|
132
|
+
- app/services/malawi_hiv_program_reports/moh/art_cohort.rb
|
133
133
|
- app/services/malawi_hiv_program_reports/moh/cohort_builder.rb
|
134
134
|
- app/services/malawi_hiv_program_reports/moh/cohort_disaggregated.rb
|
135
135
|
- app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_additions.rb
|
136
136
|
- app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_builder.rb
|
137
137
|
- app/services/malawi_hiv_program_reports/moh/cohort_struct.rb
|
138
138
|
- app/services/malawi_hiv_program_reports/moh/cohort_survival_analysis.rb
|
139
|
+
- app/services/malawi_hiv_program_reports/moh/cumulative_cohort.rb
|
140
|
+
- app/services/malawi_hiv_program_reports/moh/cumulative_outcome.rb
|
139
141
|
- app/services/malawi_hiv_program_reports/moh/moh_tpt.rb
|
140
142
|
- app/services/malawi_hiv_program_reports/moh/tpt_newly_initiated.rb
|
141
143
|
- app/services/malawi_hiv_program_reports/pepfar/defaulter_list.rb
|
@@ -187,7 +189,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
187
189
|
- !ruby/object:Gem::Version
|
188
190
|
version: '0'
|
189
191
|
requirements: []
|
190
|
-
rubygems_version: 3.
|
192
|
+
rubygems_version: 3.5.6
|
191
193
|
signing_key:
|
192
194
|
specification_version: 4
|
193
195
|
summary: Malawi HIV PROGRAM Reports for Ruby on Rails
|