malawi_hiv_program_reports 1.0.15 → 1.0.16

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ce22e02f1228c154f6012ab0a902d6840a7e1549d87e98086e5d2fd012bdf01
4
- data.tar.gz: 16039bf5d7a249f56ff008396d3e4174e46b224ec687ee44aa181512977a820c
3
+ metadata.gz: 1ac2f9f13ddc5ed6d7efe5871933b716d61f1430640cfe9a25f5a8518125ac86
4
+ data.tar.gz: b43847cef3d1c64583a7941d337055f3eda2d25f36ab6d3cd082d1e1cae227bb
5
5
  SHA512:
6
- metadata.gz: a954668190f6d5191f0e3ae7a2bad0f429d8069906df593f0965fa01e3a0415821cfa94afc79e49a1c4dae6c1ae80c17eb9e4d5c459a65a9d736b6b3a4bb5176
7
- data.tar.gz: 3bc66cb64a5fb07fcafb22391d29e828fac5cb93a5487c27087213775584239c2e6757facae70162e07bffd47ba295dd452ccb47b18001d2a27d2bacc97f9d29
6
+ metadata.gz: de81a23128d8e4c6ab5d95c29e3446cccb9a59f2ece13fc4b47a16aff1760cf706d7bceea17cd7000fec428b6325de1e40cbc033dcac6a2d95eba154f937afe3
7
+ data.tar.gz: e19999f8f28f2832ea52353da7e930fdf10f1cabee2b126da11a8b8ee45ebeda631f6f91603c0173dfd6b5073e0a6b1e820dcd0f4683b4a0b555eb9ee3bb2432
@@ -34,7 +34,7 @@ module MalawiHivProgramReports
34
34
  end
35
35
 
36
36
  def query
37
- with_lock(Cohort::LOCK_FILE) do
37
+ with_lock(Moh::Cohort::LOCK_FILE) do
38
38
  refresh_outcomes_table if @rebuild_outcomes || !outcomes_table_exists?
39
39
 
40
40
  ::Patient.find_by_sql <<~SQL
@@ -36,7 +36,7 @@ module MalawiHivProgramReports
36
36
  ].freeze
37
37
 
38
38
  def regimen_counts
39
- with_lock(Cohort::LOCK_FILE) do
39
+ with_lock(Moh::Cohort::LOCK_FILE) do
40
40
  PatientsAliveAndOnTreatment.new(start_date:, end_date:, occupation: @occupation)
41
41
  .refresh_outcomes_table
42
42
 
@@ -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 Cohort
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::Cohort,
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MalawiHivProgramReports
4
- VERSION = '1.0.15'
4
+ VERSION = '1.0.16'
5
5
  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.15
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-04-04 00:00:00.000000000 Z
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/cohort.rb
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.4.1
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