malawi_hiv_program_reports 1.0.15 → 1.0.17

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml 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