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