malawi_hiv_program_reports 1.0.15 → 1.0.17

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