malawi_hiv_program_reports 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/app/services/malawi_hiv_program_reports/README.md +16 -0
  3. data/app/services/malawi_hiv_program_reports/adapters/moh/custom.rb +199 -0
  4. data/app/services/malawi_hiv_program_reports/archiving_candidates.rb +130 -0
  5. data/app/services/malawi_hiv_program_reports/arv_refill_periods.rb +311 -0
  6. data/app/services/malawi_hiv_program_reports/clinic/README.md +5 -0
  7. data/app/services/malawi_hiv_program_reports/clinic/appointments_report.rb +317 -0
  8. data/app/services/malawi_hiv_program_reports/clinic/discrepancy_report.rb +42 -0
  9. data/app/services/malawi_hiv_program_reports/clinic/docs/hypertension_report.md +31 -0
  10. data/app/services/malawi_hiv_program_reports/clinic/drug_dispensations.rb +48 -0
  11. data/app/services/malawi_hiv_program_reports/clinic/external_consultation_clients.rb +69 -0
  12. data/app/services/malawi_hiv_program_reports/clinic/hypertension_report.rb +223 -0
  13. data/app/services/malawi_hiv_program_reports/clinic/ipt_coverage.rb +112 -0
  14. data/app/services/malawi_hiv_program_reports/clinic/ipt_report.rb +69 -0
  15. data/app/services/malawi_hiv_program_reports/clinic/lims_results.rb +55 -0
  16. data/app/services/malawi_hiv_program_reports/clinic/outcome_list.rb +127 -0
  17. data/app/services/malawi_hiv_program_reports/clinic/patients_alive_and_on_treatment.rb +57 -0
  18. data/app/services/malawi_hiv_program_reports/clinic/patients_due_for_viral_load.rb +39 -0
  19. data/app/services/malawi_hiv_program_reports/clinic/patients_on_antiretrovirals.rb +44 -0
  20. data/app/services/malawi_hiv_program_reports/clinic/patients_on_dtg.rb +36 -0
  21. data/app/services/malawi_hiv_program_reports/clinic/patients_on_treatment.rb +42 -0
  22. data/app/services/malawi_hiv_program_reports/clinic/patients_with_outdated_demographics.rb +173 -0
  23. data/app/services/malawi_hiv_program_reports/clinic/pregnant_patients.rb +91 -0
  24. data/app/services/malawi_hiv_program_reports/clinic/regimen_dispensation_data.rb +282 -0
  25. data/app/services/malawi_hiv_program_reports/clinic/regimen_switch.rb +456 -0
  26. data/app/services/malawi_hiv_program_reports/clinic/regimens_and_formulations.rb +182 -0
  27. data/app/services/malawi_hiv_program_reports/clinic/regimens_by_weight_and_gender.rb +108 -0
  28. data/app/services/malawi_hiv_program_reports/clinic/retention.rb +246 -0
  29. data/app/services/malawi_hiv_program_reports/clinic/stock_card_report.rb +65 -0
  30. data/app/services/malawi_hiv_program_reports/clinic/tpt_outcome.rb +494 -0
  31. data/app/services/malawi_hiv_program_reports/clinic/tx_rtt.rb +169 -0
  32. data/app/services/malawi_hiv_program_reports/clinic/viral_load.rb +292 -0
  33. data/app/services/malawi_hiv_program_reports/clinic/viral_load_disaggregated.rb +97 -0
  34. data/app/services/malawi_hiv_program_reports/clinic/viral_load_results.rb +175 -0
  35. data/app/services/malawi_hiv_program_reports/clinic/visits_report.rb +113 -0
  36. data/app/services/malawi_hiv_program_reports/clinic/vl_collection.rb +48 -0
  37. data/app/services/malawi_hiv_program_reports/cohort/outcomes.rb +338 -0
  38. data/app/services/malawi_hiv_program_reports/cohort/regimens.rb +69 -0
  39. data/app/services/malawi_hiv_program_reports/cohort/side_effects.rb +141 -0
  40. data/app/services/malawi_hiv_program_reports/cohort/tpt.rb +172 -0
  41. data/app/services/malawi_hiv_program_reports/moh/cohort.rb +278 -0
  42. data/app/services/malawi_hiv_program_reports/moh/cohort_builder.rb +2340 -0
  43. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated.rb +608 -0
  44. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_additions.rb +208 -0
  45. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_builder.rb +526 -0
  46. data/app/services/malawi_hiv_program_reports/moh/cohort_struct.rb +219 -0
  47. data/app/services/malawi_hiv_program_reports/moh/cohort_survival_analysis.rb +203 -0
  48. data/app/services/malawi_hiv_program_reports/moh/moh_tpt.rb +223 -0
  49. data/app/services/malawi_hiv_program_reports/moh/tpt_newly_initiated.rb +235 -0
  50. data/app/services/malawi_hiv_program_reports/pepfar/defaulter_list.rb +25 -0
  51. data/app/services/malawi_hiv_program_reports/pepfar/maternal_status.rb +29 -0
  52. data/app/services/malawi_hiv_program_reports/pepfar/patient_start_vl.rb +45 -0
  53. data/app/services/malawi_hiv_program_reports/pepfar/regimen_switch.rb +479 -0
  54. data/app/services/malawi_hiv_program_reports/pepfar/sc_arvdisp.rb +174 -0
  55. data/app/services/malawi_hiv_program_reports/pepfar/sc_curr.rb +98 -0
  56. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev.rb +163 -0
  57. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev2.rb +222 -0
  58. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev3.rb +421 -0
  59. data/app/services/malawi_hiv_program_reports/pepfar/tpt_status.rb +181 -0
  60. data/app/services/malawi_hiv_program_reports/pepfar/tx_ml.rb +181 -0
  61. data/app/services/malawi_hiv_program_reports/pepfar/tx_new.rb +202 -0
  62. data/app/services/malawi_hiv_program_reports/pepfar/tx_rtt.rb +288 -0
  63. data/app/services/malawi_hiv_program_reports/pepfar/tx_tb.rb +283 -0
  64. data/app/services/malawi_hiv_program_reports/pepfar/utils.rb +141 -0
  65. data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage.rb +414 -0
  66. data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage2.rb +433 -0
  67. data/app/services/malawi_hiv_program_reports/report_map.rb +56 -0
  68. data/app/services/malawi_hiv_program_reports/utils/README.md +8 -0
  69. data/app/services/malawi_hiv_program_reports/utils/common_sql_query_utils.rb +60 -0
  70. data/app/services/malawi_hiv_program_reports/utils/concurrency_utils.rb +53 -0
  71. data/app/services/malawi_hiv_program_reports/utils/docs/common_sql_query_utils.md +53 -0
  72. data/app/services/malawi_hiv_program_reports/utils/model_utils.rb +66 -0
  73. data/app/services/malawi_hiv_program_reports/utils/parameter_utils.rb +32 -0
  74. data/app/services/malawi_hiv_program_reports/utils/time_utils.rb +52 -0
  75. data/lib/malawi_hiv_program_reports/version.rb +1 -1
  76. metadata +74 -1
@@ -0,0 +1,433 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Pepfar
5
+ ## Viral Load Coverage Report
6
+ # 1. given the start and end dates, this report will go back 12 months using the end date
7
+ # 2. pick all clients that are due in the mentioned period
8
+ # 3. the picked clients should also include those that are new on ART 6 months before the end date
9
+ # 4. for the sample drawns available pick the latest sample drawn within the reporting period
10
+ # 5. for the results pick the latest result within the reporting period
11
+ class ViralLoadCoverage2
12
+
13
+ include Utils
14
+ include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
15
+ include MalawiHivProgramReports::Adapters::Moh::Custom
16
+ include MalawiHivProgramReports::Utils::ModelUtils
17
+ attr_reader :start_date, :end_date, :location
18
+
19
+ def initialize(start_date:, end_date:, **kwargs)
20
+ @start_date = start_date&.to_date
21
+ raise InvalidParameterError, 'start_date is required' unless @start_date
22
+
23
+ @end_date = end_date&.to_date || @start_date + 12.months
24
+ raise InvalidParameterError, "start_date can't be greater than end_date" if @start_date > @end_date
25
+
26
+ @occupation = kwargs.delete(:occupation)
27
+ @type = kwargs.delete(:application)
28
+ @location = kwargs.delete(:location)
29
+ end
30
+
31
+ def find_report
32
+ report = init_report
33
+ build_report(report)
34
+ report
35
+ end
36
+
37
+ def vl_maternal_status(patient_list)
38
+ return { FP: [], FBf: [] } if patient_list.blank?
39
+
40
+ pregnant = pregnant_women(patient_list).map { |woman| woman['person_id'].to_i }
41
+ return { FP: pregnant, FBf: [] } if (patient_list - pregnant).blank?
42
+
43
+ feeding = breast_feeding(patient_list - pregnant).map { |woman| woman['person_id'].to_i }
44
+
45
+ {
46
+ FP: pregnant,
47
+ FBf: feeding
48
+ }
49
+ end
50
+
51
+ # rubocop:disable Metrics/AbcSize
52
+ # rubocop:disable Metrics/CyclomaticComplexity
53
+ # rubocop:disable Metrics/MethodLength
54
+ def process_due_people
55
+ @clients = []
56
+ start = Time.now
57
+ results = clients_on_art
58
+ # get all clients that are females from results
59
+ @maternal_status = vl_maternal_status(results.map do |patient|
60
+ patient['patient_id'] if patient['gender'] == 'F'
61
+ end.compact)
62
+ if @type.blank? || @type == 'poc'
63
+ Parallel.each(results, in_threads: 20) do |patient|
64
+ process_client_eligibility(patient)
65
+ end
66
+ end
67
+ results.each { |patient| process_client_eligibility(patient) } if @type == 'emastercard'
68
+ end_time = Time.now
69
+ Rails.logger.info "Time taken to process #{results.length} clients: #{end_time - start} seconds.
70
+ These are the clients returned: #{@clients.length}"
71
+ @clients
72
+ end
73
+ # rubocop:enable Metrics/AbcSize
74
+ # rubocop:enable Metrics/CyclomaticComplexity
75
+ # rubocop:enable Metrics/MethodLength
76
+
77
+ private
78
+
79
+ # rubocop:disable Metrics/AbcSize
80
+ # rubocop:disable Metrics/MethodLength
81
+ # rubocop:disable Metrics/CyclomaticComplexity
82
+ # rubocop:disable Metrics/PerceivedComplexity
83
+ # rubocop:disable Layout/LineLength
84
+ def process_client_eligibility(patient)
85
+ result = extra_information(patient['patient_id'])
86
+ patient['defaulter_date'] = result['defaulter_date']
87
+ patient['current_regimen'] = result['current_regimen']
88
+ patient['art_start_date'] = result['art_start_date']
89
+ patient['maternal_status'] =
90
+ if @maternal_status[:FP].include?(patient['patient_id'])
91
+ 'FP'
92
+ else
93
+ (@maternal_status[:FBf].include?(patient['patient_id']) ? 'FBf' : nil)
94
+ end
95
+ return if !patient['defaulter_date'].blank? && (patient['defaulter_date'].to_date < end_date - 12.months)
96
+ return if result['art_start_date'].blank?
97
+ return if result['art_start_date'].to_date > end_date - 6.months
98
+ return if remove_adverse_outcome_patient?(patient)
99
+
100
+ @clients << patient
101
+ end
102
+
103
+ def remove_adverse_outcome_patient?(patient)
104
+ return false unless adverse_outcomes.include?(patient['state'].to_i)
105
+
106
+ last_date = patient['vl_order_date'] || patient['art_start_date']
107
+ if patient['vl_order_date'].present? && last_date.to_date >= start_date && last_date.to_date <= end_date
108
+ return false
109
+ end
110
+
111
+ length = 12
112
+ length = 6 if patient['maternal_status'] == 'FP'
113
+ length = 6 if patient['maternal_status'] == 'FBf'
114
+ length = 6 if patient['current_regimen'].to_s.match(/P/i)
115
+
116
+ if patient['vl_order_date'] && patient['vl_order_date'].to_date >= end_date - 12.months && patient['vl_order_date'].to_date <= end_date
117
+ return false
118
+ end
119
+ return false if last_date.to_date + length.months < patient['outcome_date'].to_date
120
+
121
+ true
122
+ end
123
+
124
+ def pregnant_women(patient_list)
125
+ ActiveRecord::Base.connection.select_all <<~SQL
126
+ SELECT o.person_id, o.value_coded
127
+ FROM obs o
128
+ INNER JOIN encounter e ON e.encounter_id = o.encounter_id AND e.voided = 0 AND e.encounter_type IN (#{encounter_types.to_sql})
129
+ INNER JOIN person p ON o.person_id = e.patient_id AND LEFT(p.gender, 1) = 'F'
130
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
131
+ INNER JOIN (
132
+ SELECT person_id, MAX(obs_datetime) AS obs_datetime
133
+ FROM obs
134
+ INNER JOIN encounter ON encounter.encounter_id = obs.encounter_id AND encounter.encounter_type IN (#{encounter_types.to_sql}) AND encounter.voided = 0
135
+ WHERE obs.concept_id IN (#{pregnant_concepts.to_sql})
136
+ AND obs.obs_datetime BETWEEN DATE(#{ActiveRecord::Base.connection.quote(start_date)}) AND DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
137
+ AND obs.voided = 0
138
+ #{site_manager(operator: 'AND', column: 'obs.site_id', location: @location)}
139
+ GROUP BY person_id
140
+ ) AS max_obs ON max_obs.person_id = o.person_id AND max_obs.obs_datetime = o.obs_datetime
141
+ WHERE o.concept_id IN (#{pregnant_concepts.to_sql})
142
+ AND o.voided = 0
143
+ AND o.value_coded IN (#{yes_concepts.join(',')})
144
+ AND o.person_id IN (#{patient_list.join(',')})
145
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
146
+ GROUP BY o.person_id
147
+ SQL
148
+ end
149
+
150
+ def breast_feeding(patient_list)
151
+ ActiveRecord::Base.connection.select_all <<~SQL
152
+ SELECT o.person_id, o.value_coded
153
+ FROM obs o
154
+ INNER JOIN encounter e ON e.encounter_id = o.encounter_id AND e.voided = 0 AND e.encounter_type IN (#{encounter_types.to_sql})
155
+ INNER JOIN person p ON o.person_id = e.patient_id AND LEFT(p.gender, 1) = 'F'
156
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
157
+ INNER JOIN (
158
+ SELECT person_id, MAX(obs_datetime) AS obs_datetime
159
+ FROM obs
160
+ INNER JOIN encounter ON encounter.encounter_id = obs.encounter_id AND encounter.encounter_type IN (#{encounter_types.to_sql}) AND encounter.voided = 0
161
+ WHERE obs.concept_id IN (#{breast_feeding_concepts.to_sql})
162
+ AND obs.obs_datetime BETWEEN DATE(#{ActiveRecord::Base.connection.quote(start_date)}) AND DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
163
+ AND obs.voided = 0
164
+ #{site_manager(operator: 'AND', column: 'obs.site_id', location: @location)}
165
+ GROUP BY person_id
166
+ ) AS max_obs ON max_obs.person_id = o.person_id AND max_obs.obs_datetime = o.obs_datetime
167
+ WHERE o.concept_id IN (#{breast_feeding_concepts.to_sql})
168
+ AND o.voided = 0
169
+ AND o.value_coded IN (#{yes_concepts.join(',')})
170
+ AND o.person_id IN (#{patient_list.join(',')})
171
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
172
+ GROUP BY o.person_id
173
+ SQL
174
+ end
175
+
176
+ def build_report(report)
177
+ refresh_outcomes_table
178
+ load_tx_curr_into_report(report, create_patients_alive_and_on_art_query)
179
+ clients = process_due_people
180
+ clients.each do |patient|
181
+ report[patient['age_group']][patient['gender'].to_sym][:due_for_vl] << patient['patient_id']
182
+ end
183
+ load_patient_tests_into_report(report, clients.map { |patient| patient['patient_id'] })
184
+ end
185
+
186
+ def refresh_outcomes_table
187
+ MalawiHivProgramReports::Moh::CohortBuilder.new(outcomes_definition: 'pepfar', location: @location)
188
+ .init_temporary_tables(@start_date, @end_date, nil)
189
+ end
190
+
191
+ def create_patients_alive_and_on_art_query
192
+ ActiveRecord::Base.connection.select_all(
193
+ <<~SQL
194
+ SELECT tpo.patient_id, LEFT(tesd.gender, 1) AS gender, disaggregated_age_group(tesd.birthdate, DATE('#{end_date.to_date}')) age_group
195
+ FROM temp_patient_outcomes tpo
196
+ INNER JOIN temp_earliest_start_date tesd ON tesd.patient_id = tpo.patient_id
197
+ WHERE tpo.cum_outcome = 'On antiretrovirals'
198
+ SQL
199
+ )
200
+ end
201
+
202
+ def load_tx_curr_into_report(report, patients)
203
+ report.each do |age_group, _data|
204
+ %i[M F].each do |gender|
205
+ report[age_group][gender][:tx_curr] ||= []
206
+ report[age_group][gender][:tx_curr] = populate_tx_curr(patients, age_group, gender) || []
207
+ end
208
+ end
209
+ end
210
+
211
+ def populate_tx_curr(patients, age_group, gender)
212
+ patients.select do |patient|
213
+ (patient['age_group'] == age_group && patient['gender'].to_sym == gender) && patient['patient_id']
214
+ end&.map {|a| a['patient_id']}
215
+ end
216
+
217
+ # rubocop:disable Metrics/AbcSize
218
+ # rubocop:disable Metrics/MethodLength
219
+ def load_patient_tests_into_report(report, clients)
220
+ find_patients_with_viral_load(clients).each do |patient|
221
+ age_group = patient['age_group']
222
+ gender = patient['gender'].to_sym
223
+ reason_for_test = (patient['reason_for_test'] || 'Routine').match?(/Routine/i) ? :routine : :targeted
224
+
225
+ report[age_group][gender][:drawn][reason_for_test] << patient['patient_id']
226
+ next unless patient['result_value']
227
+
228
+ if patient['result_value'].casecmp?('LDL')
229
+ report[age_group][gender][:low_vl][reason_for_test] << patient['patient_id']
230
+ elsif patient['result_value'].to_i < 1000
231
+ report[age_group][gender][:low_vl][reason_for_test] << patient['patient_id']
232
+ else
233
+ report[age_group][gender][:high_vl][reason_for_test] << patient['patient_id']
234
+ end
235
+ end
236
+ end
237
+ # rubocop:enable Metrics/AbcSize
238
+ # rubocop:enable Metrics/MethodLength
239
+
240
+ ## This method prepares the response structure for the report
241
+ def init_report
242
+ pepfar_age_groups.each_with_object({}) do |age_group, report|
243
+ report[age_group] = %i[F M].each_with_object({}) do |gender, hash|
244
+ hash[gender] = {
245
+ due_for_vl: [],
246
+ drawn: { routine: [], targeted: [] },
247
+ high_vl: { routine: [], targeted: [] },
248
+ low_vl: { routine: [], targeted: [] }
249
+ }
250
+ end
251
+ end
252
+ end
253
+
254
+ def due_for_viral_load
255
+ ActiveRecord::Base.connection.select_all <<~SQL
256
+ (#{find_patients_with_overdue_viral_load}) UNION (#{find_patients_due_for_initial_viral_load})
257
+ SQL
258
+ end
259
+
260
+ def adverse_outcomes
261
+ @adverse_outcomes ||= ActiveRecord::Base.connection.select_all(
262
+ <<~SQL
263
+ SELECT pws.program_workflow_state_id state
264
+ FROM program_workflow pw
265
+ INNER JOIN concept_name pcn ON pcn.concept_id = pw.concept_id AND pcn.concept_name_type = 'FULLY_SPECIFIED' AND pcn.voided = 0
266
+ INNER JOIN program_workflow_state pws ON pws.program_workflow_id = pw.program_workflow_id AND pws.retired = 0
267
+ INNER JOIN concept_name cn ON cn.concept_id = pws.concept_id AND cn.concept_name_type = 'FULLY_SPECIFIED' AND cn.voided = 0
268
+ WHERE pw.program_id = 1 AND pw.retired = 0 AND pws.terminal = 1
269
+ SQL
270
+ ).map { |state| state['state'] }
271
+ end
272
+
273
+ def clients_on_art
274
+ ActiveRecord::Base.connection.select_all <<~SQL
275
+ SELECT
276
+ ab.patient_id,
277
+ disaggregated_age_group(p.birthdate, DATE(#{ActiveRecord::Base.connection.quote(end_date)})) AS age_group,
278
+ p.birthdate,
279
+ p.gender,
280
+ pid.identifier AS arv_number,
281
+ current_state.state,
282
+ current_state.start_date outcome_date,
283
+ current_order.start_date vl_order_date
284
+ FROM orders ab
285
+ INNER JOIN person p ON p.person_id = ab.patient_id AND p.voided = 0
286
+ #{site_manager(operator: 'AND', column: 'p.site_id', location: @location)}
287
+ INNER JOIN drug_order dor ON dor.order_id = ab.order_id AND dor.quantity > 0
288
+ INNER JOIN arv_drug ad ON dor.drug_inventory_id = ad.drug_id
289
+ INNER JOIN patient_program pp ON pp.patient_id = ab.patient_id AND pp.voided = 0 AND pp.program_id = 1
290
+ INNER JOIN (
291
+ SELECT a.patient_program_id, a.state, a.start_date, a.end_date
292
+ FROM patient_state a
293
+ LEFT OUTER JOIN patient_state b ON a.patient_program_id = b.patient_program_id
294
+ AND a.start_date < b.start_date
295
+ AND b.voided = 0
296
+ WHERE b.patient_program_id IS NULL AND a.end_date IS NULL AND a.voided = 0
297
+ ) current_state ON current_state.patient_program_id = pp.patient_program_id
298
+ LEFT OUTER JOIN orders b ON ab.patient_id = b.patient_id
299
+ AND ab.order_id = b.order_id
300
+ AND ab.auto_expire_date < b.auto_expire_date
301
+ AND b.voided = 0 AND b.order_type_id = 1
302
+ #{site_manager(operator: 'AND', column: 'b.site_id', location: @location)}
303
+ LEFT JOIN (#{current_occupation_query}) a ON a.person_id = ab.patient_id
304
+ #{site_manager(operator: 'AND', column: 'a.site_id', location: @location)}
305
+ LEFT JOIN patient_identifier pid ON pid.patient_id = pp.patient_id AND pid.identifier_type IN (#{pepfar_patient_identifier_type.to_sql}) AND pid.voided = 0
306
+ #{site_manager(operator: 'AND', column: 'pid.site_id', location: @location)}
307
+ LEFT JOIN (
308
+ SELECT ab.patient_id, MAX(ab.start_date) start_date
309
+ FROM orders ab
310
+ INNER JOIN concept_name
311
+ ON concept_name.concept_id = ab.concept_id
312
+ AND concept_name.name IN ('Blood', 'DBS (Free drop to DBS card)', 'DBS (Using capillary tube)', '50:50 Normal Plasma')
313
+ AND concept_name.voided = 0
314
+ #{site_manager(operator: 'AND', column: 'ab.site_id', location: @location)}
315
+ LEFT OUTER JOIN orders b ON ab.patient_id = b.patient_id
316
+ AND ab.order_id = b.order_id
317
+ AND ab.start_date < b.start_date
318
+ AND b.voided = 0
319
+ #{site_manager(operator: 'AND', column: 'b.site_id', location: @location)}
320
+ WHERE b.patient_id IS NULL AND ab.voided = 0 AND ab.order_type_id = 4 AND ab.start_date < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
321
+ GROUP BY ab.patient_id
322
+ ) current_order ON current_order.patient_id = ab.patient_id
323
+ WHERE b.patient_id IS NULL
324
+ AND ab.voided = 0 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
325
+ AND ab.start_date < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
326
+ AND p.person_id NOT IN (#{drug_refills_and_external_consultation_list})
327
+ #{site_manager(operator: 'AND', column: 'ab.site_id', location: @location)}
328
+ AND ((current_state.state IN (#{adverse_outcomes.join(',')}) AND current_state.start_date >= (DATE(#{ActiveRecord::Base.connection.quote(end_date)}) - INTERVAL 12 MONTH)) OR current_state.state IN (7, 1, 87, 120, 136))
329
+ GROUP BY ab.patient_id;
330
+ SQL
331
+ end
332
+
333
+ def extra_information(patient_id)
334
+ ActiveRecord::Base.connection.select_one <<~SQL
335
+ SELECT #{function_manager(function: 'patient_current_regimen', location: @location, args: "#{patient_id}, DATE(#{ActiveRecord::Base.connection.quote(end_date)}), #{@location}")} AS current_regimen,
336
+ #{function_manager(function: 'date_antiretrovirals_started', location: @location, args: "#{patient_id}, DATE(#{ActiveRecord::Base.connection.quote(end_date)}), #{@location}")} AS art_start_date,
337
+ #{function_manager(function: 'current_pepfar_defaulter_date', location: @location, args: "#{patient_id}, DATE(#{ActiveRecord::Base.connection.quote(end_date)}), #{@location}")} AS defaulter_date
338
+ SQL
339
+ end
340
+
341
+ ##
342
+ # Find all patients that are on treatment with at least one VL before end of reporting period.
343
+ def find_patients_with_viral_load(clients)
344
+ ActiveRecord::Base.connection.select_all <<~SQL
345
+ SELECT orders.patient_id,
346
+ disaggregated_age_group(patient.birthdate,
347
+ DATE(#{ActiveRecord::Base.connection.quote(end_date)})) AS age_group,
348
+ patient.birthdate,
349
+ patient.gender,
350
+ patient_identifier.identifier AS arv_number,
351
+ orders.start_date AS order_date,
352
+ COALESCE(orders.discontinued_date, orders.start_date) AS sample_draw_date,
353
+ COALESCE(reason_for_test_value.name, reason_for_test.value_text) AS reason_for_test,
354
+ result.value_modifier AS result_modifier,
355
+ COALESCE(result.value_numeric, result.value_text) AS result_value
356
+ FROM orders
357
+ INNER JOIN person patient ON patient.person_id = orders.patient_id AND patient.voided = 0
358
+ #{site_manager(operator: 'AND', column: 'patient.site_id', location: @location)}
359
+ INNER JOIN order_type
360
+ ON order_type.order_type_id = orders.order_type_id
361
+ AND order_type.name = 'Lab'
362
+ AND order_type.retired = 0
363
+ INNER JOIN concept_name
364
+ ON concept_name.concept_id = orders.concept_id
365
+ AND concept_name.name IN ('Blood', 'DBS (Free drop to DBS card)', 'DBS (Using capillary tube)', 'Plasma')
366
+ AND concept_name.voided = 0
367
+ LEFT JOIN obs AS reason_for_test
368
+ ON reason_for_test.order_id = orders.order_id
369
+ AND reason_for_test.concept_id IN (SELECT concept_id FROM concept_name WHERE name LIKE 'Reason for test' AND voided = 0)
370
+ AND reason_for_test.voided = 0
371
+ LEFT JOIN concept_name AS reason_for_test_value
372
+ ON reason_for_test_value.concept_id = reason_for_test.value_coded
373
+ AND reason_for_test_value.voided = 0
374
+ LEFT JOIN obs AS result
375
+ ON result.order_id = orders.order_id
376
+ AND result.concept_id IN (SELECT concept_id FROM concept_name WHERE name LIKE 'HIV Viral load' AND voided = 0)
377
+ AND result.voided = 0
378
+ AND (result.value_text IS NOT NULL OR result.value_numeric IS NOT NULL)
379
+ INNER JOIN (
380
+ /* Get the latest order dates for each patient */
381
+ SELECT orders.patient_id, MAX(orders.start_date) AS start_date
382
+ FROM orders
383
+ INNER JOIN order_type
384
+ ON order_type.order_type_id = orders.order_type_id
385
+ AND order_type.name = 'Lab'
386
+ AND order_type.retired = 0
387
+ INNER JOIN concept_name
388
+ ON concept_name.concept_id = orders.concept_id
389
+ AND concept_name.name IN ('Blood', 'DBS (Free drop to DBS card)', 'DBS (Using capillary tube)', 'Plasma')
390
+ AND concept_name.voided = 0
391
+ WHERE orders.start_date < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
392
+ #{site_manager(operator: 'AND', column: 'orders.site_id', location: @location)}
393
+ AND orders.start_date >= DATE(#{ActiveRecord::Base.connection.quote(start_date)}) - INTERVAL 12 MONTH
394
+ AND orders.voided = 0
395
+ GROUP BY orders.patient_id
396
+ ) AS latest_patient_order_date
397
+ ON latest_patient_order_date.patient_id = orders.patient_id
398
+ AND latest_patient_order_date.start_date = orders.start_date
399
+ LEFT JOIN patient_identifier
400
+ ON patient_identifier.patient_id = orders.patient_id
401
+ AND patient_identifier.identifier_type IN (#{pepfar_patient_identifier_type.to_sql})
402
+ AND patient_identifier.voided = 0
403
+ WHERE orders.start_date < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
404
+ AND orders.start_date >= DATE(#{ActiveRecord::Base.connection.quote(start_date)}) - INTERVAL 12 MONTH
405
+ AND orders.voided = 0
406
+ AND orders.patient_id IN (#{clients.push(0).join(',')})
407
+ GROUP BY orders.patient_id
408
+ SQL
409
+ end
410
+
411
+ def yes_concepts
412
+ @yes_concepts ||= ::ConceptName.where(name: 'Yes').select(:concept_id).map do |record|
413
+ record['concept_id'].to_i
414
+ end
415
+ end
416
+
417
+ def pregnant_concepts
418
+ @pregnant_concepts ||= ::ConceptName.where(name: ['Is patient pregnant?', 'patient pregnant'])
419
+ .select(:concept_id)
420
+ end
421
+
422
+ def breast_feeding_concepts
423
+ @breast_feeding_concepts ||= ::ConceptName.where(name: ['Breast feeding?', 'Breast feeding', 'Breastfeeding'])
424
+ .select(:concept_id)
425
+ end
426
+
427
+ def encounter_types
428
+ @encounter_types ||= ::EncounterType.where(name: ['HIV CLINIC CONSULTATION', 'HIV STAGING'])
429
+ .select(:encounter_type_id)
430
+ end
431
+ end
432
+ end
433
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module ReportMap
5
+ REPORTS = {
6
+ 'ARCHIVING_CANDIDATES' => MalawiHivProgramReports::ArchivingCandidates,
7
+ 'APPOINTMENTS' => MalawiHivProgramReports::Clinic::AppointmentsReport,
8
+ 'ARV_REFILL_PERIODS' => MalawiHivProgramReports::ArvRefillPeriods,
9
+ 'COHORT' => MalawiHivProgramReports::Moh::Cohort,
10
+ 'COHORT_DISAGGREGATED' => MalawiHivProgramReports::Moh::CohortDisaggregated,
11
+ 'COHORT_DISAGGREGATED_ADDITIONS' => MalawiHivProgramReports::Moh::CohortDisaggregatedAdditions,
12
+ 'COHORT_SURVIVAL_ANALYSIS' => MalawiHivProgramReports::Moh::CohortSurvivalAnalysis,
13
+ 'DRUG_DISPENSATIONS' => MalawiHivProgramReports::Clinic::DrugDispensations,
14
+ 'HIGH_VL_PATIENTS' => MalawiHivProgramReports::Clinic::ViralLoadResults,
15
+ 'IPT' => MalawiHivProgramReports::Clinic::IptReport,
16
+ 'PATIENTS_WITH_OUTDATED_DEMOGRAPHICS' => MalawiHivProgramReports::Clinic::PatientsWithOutdatedDemographics,
17
+ 'PATIENTS_ON_DTG' => MalawiHivProgramReports::Clinic::PatientsOnDtg,
18
+ 'PREGNANT_PATIENTS' => MalawiHivProgramReports::Clinic::PregnantPatients,
19
+ 'REGIMENS_AND_FORMULATIONS' => MalawiHivProgramReports::Clinic::RegimensAndFormulations,
20
+ 'REGIMENS_BY_WEIGHT_AND_GENDER' => MalawiHivProgramReports::Clinic::RegimensByWeightAndGender,
21
+ 'REGIMEN_SWITCH' => MalawiHivProgramReports::Clinic::RegimenSwitch,
22
+ 'REGIMEN_REPORT' => MalawiHivProgramReports::Clinic::RegimenDispensationData,
23
+ 'PEPFAR_REGIMEN_SWITCH' => MalawiHivProgramReports::Pepfar::RegimenSwitch,
24
+ 'RETENTION' => MalawiHivProgramReports::Clinic::Retention,
25
+ 'LIMS_ELECTRONIC_RESULTS' => MalawiHivProgramReports::Clinic::LimsResults,
26
+ 'TPT_OUTCOME' => MalawiHivProgramReports::Clinic::TptOutcome,
27
+ 'CLINIC_TX_RTT' => MalawiHivProgramReports::Clinic::TxRtt,
28
+ 'TB_PREV2' => MalawiHivProgramReports::Pepfar::TbPrev3,
29
+ 'TPT_NEWLY_INITIATED' => MalawiHivProgramReports::Moh::TptNewlyInitiated,
30
+ 'TX_CURR' => MalawiHivProgramReports::Clinic::PatientsAliveAndOnTreatment,
31
+ 'TX_ML' => MalawiHivProgramReports::Pepfar::TxMl,
32
+ 'TX_RTT' => MalawiHivProgramReports::Pepfar::TxRtt,
33
+ 'IPT_COVERAGE' => MalawiHivProgramReports::Clinic::IptCoverage,
34
+ 'VISITS' => MalawiHivProgramReports::Clinic::VisitsReport,
35
+ 'VL_DUE' => MalawiHivProgramReports::Clinic::PatientsDueForViralLoad,
36
+ 'DEFAULTER_LIST' => MalawiHivProgramReports::Pepfar::DefaulterList,
37
+ 'VL_DISAGGREGATED' => MalawiHivProgramReports::Clinic::ViralLoadDisaggregated,
38
+ 'TB_PREV' => MalawiHivProgramReports::Pepfar::TbPrev,
39
+ 'OUTCOME_LIST' => MalawiHivProgramReports::Clinic::OutcomeList,
40
+ 'VIRAL_LOAD' => MalawiHivProgramReports::Clinic::ViralLoad,
41
+ 'VIRAL_LOAD_COVERAGE' => MalawiHivProgramReports::Pepfar::ViralLoadCoverage2,
42
+ 'VL_MATERNAL_STATUS' => MalawiHivProgramReports::Pepfar::MaternalStatus,
43
+ 'EXTERNAL_CONSULTATION_CLIENTS' => MalawiHivProgramReports::Clinic::ExternalConsultationClients,
44
+ 'SC_ARVDISP' => MalawiHivProgramReports::Pepfar::ScArvdisp,
45
+ 'SC_CURR' => MalawiHivProgramReports::Pepfar::ScCurr,
46
+ 'PATIENT_ART_VL_DATES' => MalawiHivProgramReports::Pepfar::PatientStartVl,
47
+ 'MOH_TPT' => MalawiHivProgramReports::Moh::MohTpt,
48
+ 'TX_TB' => MalawiHivProgramReports::Pepfar::TxTb,
49
+ 'VL_COLLECTION' => MalawiHivProgramReports::Clinic::VlCollection,
50
+ 'DISCREPANCY_REPORT' => MalawiHivProgramReports::Clinic::DiscrepancyReport,
51
+ 'STOCK_CARD' => MalawiHivProgramReports::Clinic::StockCardReport,
52
+ 'HYPERTENSION_REPORT' => MalawiHivProgramReports::Clinic::HypertensionReport,
53
+ 'TX_NEW' => MalawiHivProgramReports::Pepfar::TxNew
54
+ }.freeze
55
+ end
56
+ end
@@ -0,0 +1,8 @@
1
+ # About UTILS
2
+ This folder contains all the utility functions that are used in the project. The functions are divided into different files based on their functionality. The files are as follows:
3
+
4
+ - [Common SQL Queries Utils](docs/common_sql_query_utils.md) - This file contains all the common SQL queries that are used in the project.
5
+ - [Concurrency Utils]() - This file contains all the functions that are used to handle concurrency in the project.
6
+ - [Model Utils]() - This file contains all the functions that are used to handle the models in the project.
7
+ - [Parameter Utils]() - This file contains all the functions that are used to handle the parameters in the project.
8
+ - [Time Utils]() - This file contains all the functions that are used to handle the time in the project.
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is a module that can be included in any class that needs to use the methods defined here.
4
+ module MalawiHivProgramReports
5
+ module Utils
6
+ module CommonSqlQueryUtils
7
+ def process_occupation(start_date:, end_date:, occupation:, location:, definition: 'moh')
8
+ return if occupation.blank?
9
+
10
+ MalawiHivProgramReports::Moh::CohortBuilder.new(outcomes_definition: definition, location:).init_temporary_tables(start_date, end_date,
11
+ occupation)
12
+ end
13
+
14
+ def occupation_filter(occupation:, field_name:, table_name: '', include_clause: true)
15
+ clause = 'WHERE' if include_clause
16
+ table_name = "#{table_name}." unless table_name.blank?
17
+ return '' if occupation.blank?
18
+ return '' if occupation == 'All'
19
+ if occupation == 'Military'
20
+ return "#{clause} #{table_name}#{field_name} IN ('#{occupation}', 'MDF Reserve', 'MDF Retired', 'Soldier', 'Soldier/Police')"
21
+ end
22
+ return unless occupation == 'Civilian'
23
+
24
+ "#{clause} #{table_name}#{field_name} NOT IN ('Military', 'MDF Reserve', 'MDF Retired', 'Soldier', 'Soldier/Police')"
25
+ end
26
+
27
+ def external_client_query(end_date:)
28
+ end_date = ActiveRecord::Base.connection.quote(end_date)
29
+ <<~SQL
30
+ SELECT obs.person_id FROM obs,
31
+ (SELECT person_id, Max(obs_datetime) AS obs_datetime, concept_id FROM obs
32
+ WHERE concept_id IN (SELECT concept_id FROM concept_name WHERE name = 'Type of patient' AND voided = 0)
33
+ AND DATE(obs_datetime) <= #{end_date}
34
+ AND voided = 0
35
+ GROUP BY person_id,concept_id) latest_record
36
+ WHERE obs.person_id = latest_record.person_id
37
+ AND obs.concept_id = latest_record.concept_id
38
+ AND obs.obs_datetime = latest_record.obs_datetime
39
+ AND obs.value_coded IN (SELECT concept_id FROM concept_name WHERE name = 'Drug refill' || name = 'External consultation')
40
+ AND obs.voided = 0
41
+ #{site_manager(operator: 'AND', column: 'obs.site_id', location: @location)}
42
+ SQL
43
+ end
44
+
45
+ def current_occupation_query
46
+ ActiveRecord::Base.connection.adapter_name.downcase
47
+ <<~SQL
48
+ SELECT a.person_id, a.value, a.site_id
49
+ FROM person_attribute a
50
+ LEFT OUTER JOIN person_attribute b
51
+ ON a.person_attribute_id = b.person_attribute_id #{site_manager(operator: 'AND', column: 'b.site_id', location: @location)}
52
+ AND a.date_created < b.date_created
53
+ AND b.voided = 0 #{site_manager(operator: 'AND', column: 'a.site_id', location: @location)}
54
+ WHERE b.person_attribute_id IS NULL AND a.person_attribute_type_id = 13 AND a.voided = 0
55
+ #{site_manager(operator: 'AND', column: 'a.site_id', location: @location)}
56
+ SQL
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Utils
5
+ module ConcurrencyUtils
6
+ LOCK_FILES_DIR_PATH = Rails.root.join('tmp', 'locks')
7
+
8
+ ##
9
+ # Acquire a lock and run the given block of code.
10
+ #
11
+ # The locking mechanism uses files to allow for blocking across processes.
12
+ # This is useful for example in situations where you only want to run one
13
+ # instance of a report.
14
+ #
15
+ # Parameters:
16
+ # lock_file_path: A relative path to the lock file (allows for namespacing your locks, eg art_service/regimens.lock)
17
+ # blocking: If lock can not be acquired wait else throw an error (defaults to true)
18
+ #
19
+ # Raises:
20
+ # FailedToAcquireLock: When lock couldn't be acquired and blocking is set to false
21
+ #
22
+ # Usage:
23
+ # class Someclass
24
+ # include ModelUtils
25
+ #
26
+ # def do_something
27
+ # with_lock('mylockfile.lock') do
28
+ # # Run some task requiring exclusive access to some resource
29
+ # end
30
+ # end
31
+ # end
32
+ #
33
+ # SomeClass.new.do_something
34
+ def with_lock(lock_file_path, blocking: true)
35
+ path = LOCK_FILES_DIR_PATH.join(lock_file_path)
36
+
37
+ unless Dir.exist?(path.dirname)
38
+ Rails.logger.debug("Creating lock file directory: #{path.dirname}")
39
+ ::FileUtils.mkdir_p(path.dirname)
40
+ end
41
+
42
+ File.open(path, 'w') do |lock_file|
43
+ Rails.logger.debug("Attempting to acquire lock: #{lock_file_path}")
44
+ locked = lock_file.flock(blocking ? File::LOCK_EX : File::LOCK_NB | File::LOCK_EX)
45
+ raise ::FailedToAcquireLock, "Lock #{lock_file_path} is locked by another process" if !locked && !blocking
46
+
47
+ lock_file.write("Locked by process ##{Process.pid} at #{Time.now}")
48
+ yield
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,53 @@
1
+ # Code Documentation
2
+ ## Summary
3
+ The documentation is for a module called ```CommonSqlQueryUtils``` that provides two methods: ```occupation_filter``` and ```external_client_query```. The ```occupation_filter``` method takes in parameters such as occupation, field_name, table_name, and include_clause, and returns a SQL clause based on the occupation value. The ```external_client_query``` method takes in the end_date parameter and returns a SQL query that retrieves person_ids from the ```obs``` table based on certain conditions.
4
+
5
+ Example Usage
6
+ ### Example usage of the ```occupation_filter``` method
7
+ ```ruby
8
+ include CommonSqlQueryUtils
9
+
10
+
11
+ occupation_filter(occupation: 'Military', field_name: 'occupation', table_name: 'users', include_clause: true)
12
+ ```
13
+ #### Output: "WHERE users.occupation = 'Military'"
14
+
15
+ ```ruby
16
+ occupation_filter(occupation: 'Civilian', field_name: 'occupation', table_name: 'users', include_clause: false)
17
+ ```
18
+ #### Output: "users.occupation != 'Military'"
19
+
20
+ ### Example usage of the ```external_client_query``` method
21
+ ```ruby
22
+ include CommonSqlQueryUtils
23
+
24
+ external_client_query(end_date: '2021-01-01')
25
+ ```
26
+ #### Output: SQL query string
27
+
28
+ ## Code Analysis
29
+ ### Inputs
30
+ - ```occupation``` (string): The occupation value to filter on.
31
+ - ```field_name``` (string): The name of the field to filter on.
32
+ - ```table_name``` (string): The name of the table to include in the SQL clause.
33
+ - ```include_clause``` (boolean): Whether to include the ```WHERE``` clause in the SQL statement.
34
+ - ```end_date``` (string): The end date to use in the SQL query.
35
+
36
+ ### Flow
37
+ The ```occupation_filter``` method checks if the ```include_clause``` parameter is true and sets the ```clause``` variable to ```WHERE``` if it is.
38
+ The ```table_name``` variable is modified to include a trailing dot if it is not blank.
39
+ The method then checks if the ```occupation``` parameter is blank or equal to ```All``` and returns an empty string in those cases.
40
+ If the ```occupation``` parameter is ```Military```, the method returns a SQL clause string with the ```occupation``` field equal to ```Military```.
41
+ If the ```occupation``` parameter is ```Civilian```, the method returns a SQL clause string with the ```occupation``` field not equal to ```Military```.
42
+
43
+
44
+
45
+ The ``external_client_query`` method quotes the ```end_date``` parameter using ActiveRecord::Base.connection.quote.
46
+ The method then returns a multi-line SQL query string that retrieves person_ids from the ```obs``` table based on certain conditions.
47
+
48
+
49
+ ### Outputs
50
+ The ``occupation_filter`` method returns a SQL clause string based on the occupation value.
51
+
52
+
53
+ The ``external_client_query`` method returns a multi-line SQL query string.