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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c1d535116ce1283de2cbbfd8e32f29f7c67ca4a3ffab22cf91de0fe3396199b
4
- data.tar.gz: f094051dd235262b875743e63b059ed4449d5d966b13dbfd4bb7c4fac16d0a29
3
+ metadata.gz: 1ac2f9f13ddc5ed6d7efe5871933b716d61f1430640cfe9a25f5a8518125ac86
4
+ data.tar.gz: b43847cef3d1c64583a7941d337055f3eda2d25f36ab6d3cd082d1e1cae227bb
5
5
  SHA512:
6
- metadata.gz: 7e7c15a56f50c1923fadbda4fcd7b1711989d50d5d32ca6c7f9cb9862694248194d2a9d7589538d10d894d65654af74e0d260f3ea0e7fc3754ee5238c4002092
7
- data.tar.gz: 5864f57e5cb41ccb861ef52f474c6ea4d08b105089fd84dd6322adaaaf7603a4492109d4c19f6c8c9f149ca1bdac5405280ab37e7430b5976ad5a5c27135a6ed
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.14'
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.14
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-03-27 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