malawi_hiv_program_reports 1.0.1 → 1.0.2

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.
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.