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,414 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Pepfar
5
+ class ViralLoadCoverage
6
+ attr_reader :start_date, :end_date
7
+
8
+ include Utils
9
+
10
+ def initialize(**params)
11
+ @start_date = params[:start_date]&.to_date
12
+ raise ::InvalidParameterError, 'start_date is required' unless @start_date
13
+
14
+ @end_date = params[:end_date]&.to_date || (@start_date + 12.months)
15
+ raise ::InvalidParameterError, "start_date can't be greater than end_date" if @start_date > @end_date
16
+
17
+ @tx_curr_definition = params.fetch(:tx_curr_definition, 'pepfar')&.downcase
18
+ unless %w[moh pepfar].include?(@tx_curr_definition)
19
+ raise ::InvalidParameterError, "tx_curr_definition can only moh or pepfar not #{@tx_curr_definition}"
20
+ end
21
+
22
+ @rebuild_outcomes = params.fetch(:rebuild_outcomes, 'true')&.casecmp?('true')
23
+ @type = params.fetch(:application, 'poc')
24
+ end
25
+
26
+ def find_report
27
+ report = init_report
28
+
29
+ case @type
30
+ when /poc/i then build_poc_report(report)
31
+ when /emastercard/i then build_emastercard_report(report)
32
+ else raise ::InvalidParameterError, "Report type must be one of [poc, emastercard] not #{@type}"
33
+ end
34
+
35
+ report
36
+ end
37
+
38
+ def vl_maternal_status(patient_list)
39
+ pregnant = pregnant_women(patient_list).map { |woman| woman['person_id'].to_i }
40
+ feeding = breast_feeding(patient_list - pregnant).map { |woman| woman['person_id'].to_i }
41
+
42
+ {
43
+ FP: pregnant,
44
+ FBf: feeding
45
+ }
46
+ end
47
+
48
+ private
49
+
50
+ def pregnant_women(patient_list)
51
+ encounter_types = ::EncounterType.where(name: ['HIV CLINIC CONSULTATION', 'HIV STAGING'])
52
+ .select(:encounter_type_id)
53
+
54
+ pregnant_concepts = ::ConceptName.where(name: ['Is patient pregnant?', 'patient pregnant'])
55
+ .select(:concept_id)
56
+
57
+ ActiveRecord::Base.connection.select_all <<~SQL
58
+ SELECT obs.person_id,obs.value_coded
59
+ FROM obs obs
60
+ INNER JOIN encounter enc
61
+ ON enc.encounter_id = obs.encounter_id
62
+ AND enc.voided = 0
63
+ AND enc.encounter_type IN (#{encounter_types.to_sql})
64
+ INNER JOIN temp_earliest_start_date e
65
+ ON e.patient_id = enc.patient_id
66
+ AND LEFT(e.gender, 1) = 'F'
67
+ INNER JOIN temp_patient_outcomes
68
+ ON temp_patient_outcomes.patient_id = e.patient_id
69
+ AND temp_patient_outcomes.cum_outcome = 'On antiretrovirals'
70
+ INNER JOIN (
71
+ SELECT person_id, MAX(obs_datetime) AS obs_datetime
72
+ FROM obs
73
+ INNER JOIN encounter
74
+ ON encounter.encounter_id = obs.encounter_id
75
+ AND encounter.encounter_type IN (#{encounter_types.to_sql})
76
+ AND encounter.voided = 0
77
+ WHERE concept_id IN (#{pregnant_concepts.to_sql})
78
+ AND obs_datetime BETWEEN DATE('#{@start_date}') AND DATE('#{@end_date}') + INTERVAL 1 DAY
79
+ AND obs.voided = 0
80
+ GROUP BY person_id
81
+ ) AS max_obs
82
+ ON max_obs.person_id = obs.person_id
83
+ AND max_obs.obs_datetime = obs.obs_datetime
84
+ WHERE obs.concept_id IN (#{pregnant_concepts.to_sql})
85
+ AND obs.voided = 0
86
+ AND obs.person_id IN (#{patient_list.join(',')})
87
+ GROUP BY obs.person_id
88
+ HAVING obs.value_coded = 1065
89
+ ORDER BY obs.obs_datetime DESC;
90
+ SQL
91
+ end
92
+
93
+ def breast_feeding(patient_list)
94
+ encounter_types = ::EncounterType.where(name: ['HIV CLINIC CONSULTATION', 'HIV STAGING'])
95
+ .select(:encounter_type_id)
96
+
97
+ breastfeeding_concepts = ::ConceptName.where(name: ['Breast feeding?', 'Breast feeding', 'Breastfeeding'])
98
+ .select(:concept_id)
99
+
100
+ ActiveRecord::Base.connection.select_all <<~SQL
101
+ SELECT obs.person_id,obs.value_coded
102
+ FROM obs
103
+ INNER JOIN encounter enc
104
+ ON enc.encounter_id = obs.encounter_id
105
+ AND enc.voided = 0
106
+ AND enc.encounter_type IN (#{encounter_types.to_sql})
107
+ INNER JOIN temp_earliest_start_date e
108
+ ON e.patient_id = enc.patient_id
109
+ AND LEFT(e.gender, 1) = 'F'
110
+ INNER JOIN temp_patient_outcomes
111
+ ON temp_patient_outcomes.patient_id = e.patient_id
112
+ AND temp_patient_outcomes.cum_outcome = 'On antiretrovirals'
113
+ INNER JOIN (
114
+ SELECT person_id, MAX(obs_datetime) AS obs_datetime
115
+ FROM obs
116
+ INNER JOIN encounter
117
+ ON encounter.encounter_id = obs.encounter_id
118
+ AND encounter.encounter_type IN (#{encounter_types.to_sql})
119
+ AND encounter.voided = 0
120
+ WHERE person_id IN (SELECT patient_id FROM temp_patient_outcomes WHERE cum_outcome = 'On antiretrovirals')
121
+ AND concept_id IN (#{breastfeeding_concepts.to_sql})
122
+ AND obs.voided = 0
123
+ AND obs_datetime < DATE('#{end_date}') + INTERVAL 1 DAY
124
+ GROUP BY person_id
125
+ ) AS max_obs
126
+ ON max_obs.person_id = obs.person_id
127
+ AND max_obs.obs_datetime = obs.obs_datetime
128
+ WHERE obs.person_id = e.patient_id
129
+ AND obs.person_id IN (#{patient_list.join(',')})
130
+ AND obs.obs_datetime BETWEEN DATE("#{@start_date}") AND DATE("#{@end_date}") + INTERVAL 1 DAY
131
+ AND obs.concept_id IN (#{breastfeeding_concepts.to_sql})
132
+ AND obs.voided = 0
133
+ GROUP BY obs.person_id
134
+ HAVING obs.value_coded = 1065
135
+ ORDER BY obs.obs_datetime DESC;
136
+ SQL
137
+ end
138
+
139
+ def build_poc_report(report)
140
+ find_patients_alive_and_on_art.each { |patient| report[patient['age_group']][:tx_curr] << patient }
141
+ find_patients_due_for_initial_viral_load.each do |patient|
142
+ report[patient['age_group']][:due_for_vl] << patient
143
+ end
144
+ find_patients_with_overdue_viral_load.each { |patient| report[patient['age_group']][:due_for_vl] << patient }
145
+ load_patient_tests_into_report(report)
146
+ end
147
+
148
+ def build_emastercard_report(report)
149
+ find_patients_alive_and_on_art.each { |patient| report[patient['age_group']][:tx_curr] << patient }
150
+ load_emastercard_results_into_report(report)
151
+ end
152
+
153
+ def init_report
154
+ pepfar_age_groups.each_with_object({}) do |age_group, report|
155
+ report[age_group] = {
156
+ tx_curr: [],
157
+ due_for_vl: [],
158
+ drawn: { routine: [], targeted: [] },
159
+ high_vl: { routine: [], targeted: [] },
160
+ low_vl: { routine: [], targeted: [] }
161
+ }
162
+ end
163
+ end
164
+
165
+ def load_patient_tests_into_report(report)
166
+ find_patients_with_viral_load.each do |patient|
167
+ age_group = patient['age_group']
168
+ reason_for_test = (patient['reason_for_test'] || 'Routine').match?(/Routine/i) ? :routine : :targeted
169
+
170
+ report[age_group][:drawn][reason_for_test] << patient
171
+ next unless patient['result_value']
172
+
173
+ if patient['result_value'].casecmp?('LDL')
174
+ report[age_group][:low_vl][reason_for_test] << patient
175
+ elsif patient['result_value'].to_i < 1000
176
+ report[age_group][:low_vl][reason_for_test] << patient
177
+ else
178
+ report[age_group][:high_vl][reason_for_test] << patient
179
+ end
180
+ end
181
+ end
182
+
183
+ def load_emastercard_results_into_report(report)
184
+ find_emastercard_patient_results.each do |patient|
185
+ if patient['result_value'] < 1000
186
+ report[patient['age_group']][:low_vl][:routine] << patient
187
+ else
188
+ report[patient['age_group']][:high_vl][:routine] << patient
189
+ end
190
+ end
191
+ end
192
+
193
+ def find_patients_alive_and_on_art
194
+ patients = MalawiHivProgramReports::Clinic::PatientsAliveAndOnTreatment
195
+ .new(start_date:, end_date:, outcomes_definition: @tx_curr_definition, rebuild_outcomes: @rebuild_outcomes)
196
+ .query
197
+ pepfar_patient_drilldown_information(patients, end_date).map do |patient|
198
+ {
199
+ 'patient_id' => patient.patient_id,
200
+ 'arv_number' => patient.arv_number,
201
+ 'age_group' => patient.age_group,
202
+ 'birthdate' => patient.birthdate,
203
+ 'gender' => patient.gender
204
+ }
205
+ end
206
+ end
207
+
208
+ ##
209
+ # Selects patients whose last viral load should have expired before the end of the reporting period.
210
+ #
211
+ # Patients returned by this aren't necessarily due for viral load, they may have
212
+ # their current milestone delayed. So extra processing on the patients is required
213
+ # to filter out the patients with delayed milestones.
214
+ def find_patients_with_overdue_viral_load
215
+ # Find all patients whose last order's expires in or before the reporting period (making them due)
216
+ # or patients whose first order comes at 6 months or greater after starting ART.
217
+ ActiveRecord::Base.connection.select_all <<~SQL
218
+ SELECT orders.patient_id,
219
+ disaggregated_age_group(patient.birthdate,
220
+ DATE(#{ActiveRecord::Base.connection.quote(end_date)})) AS age_group,
221
+ patient.birthdate,
222
+ patient.gender,
223
+ patient_identifier.identifier AS arv_number
224
+ FROM orders
225
+ INNER JOIN order_type
226
+ ON order_type.order_type_id = orders.order_type_id
227
+ AND order_type.name = 'Lab'
228
+ AND order_type.retired = 0
229
+ INNER JOIN concept_name
230
+ ON concept_name.concept_id = orders.concept_id
231
+ AND concept_name.name IN ('Blood', 'DBS (Free drop to DBS card)', 'DBS (Using capillary tube)')
232
+ AND concept_name.voided = 0
233
+ INNER JOIN (
234
+ /* Get the latest order dates for each patient */
235
+ SELECT orders.patient_id, MAX(orders.start_date) AS start_date
236
+ FROM orders
237
+ INNER JOIN order_type
238
+ ON order_type.order_type_id = orders.order_type_id
239
+ AND order_type.name = 'Lab'
240
+ AND order_type.retired = 0
241
+ INNER JOIN concept_name
242
+ ON concept_name.concept_id = orders.concept_id
243
+ AND concept_name.name IN ('Blood', 'DBS (Free drop to DBS card)', 'DBS (Using capillary tube)')
244
+ AND concept_name.voided = 0
245
+ WHERE orders.start_date <= DATE(#{ActiveRecord::Base.connection.quote(end_date)}) - INTERVAL 12 MONTH
246
+ AND orders.voided = 0
247
+ GROUP BY orders.patient_id
248
+ ) AS latest_patient_order_date
249
+ ON latest_patient_order_date.patient_id = orders.patient_id
250
+ AND latest_patient_order_date.start_date = orders.start_date
251
+ INNER JOIN temp_earliest_start_date AS patient ON patient.patient_id = orders.patient_id
252
+ INNER JOIN temp_patient_outcomes AS outcomes
253
+ ON outcomes.patient_id = patient.patient_id
254
+ AND outcomes.cum_outcome = 'On antiretrovirals'
255
+ LEFT JOIN patient_identifier
256
+ ON patient_identifier.patient_id = orders.patient_id
257
+ AND patient_identifier.identifier_type IN (#{pepfar_patient_identifier_type.to_sql})
258
+ AND patient_identifier.voided = 0
259
+ WHERE orders.start_date < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) - INTERVAL 12 MONTH
260
+ GROUP BY orders.patient_id
261
+ SQL
262
+ end
263
+
264
+ ##
265
+ # Returns all patients that have been on ART for at least 6 months and have never had a Viral Load.
266
+ def find_patients_due_for_initial_viral_load
267
+ ActiveRecord::Base.connection.select_all <<~SQL
268
+ SELECT patient.patient_id,
269
+ disaggregated_age_group(patient.birthdate,
270
+ DATE(#{ActiveRecord::Base.connection.quote(end_date)})) AS age_group,
271
+ patient.birthdate,
272
+ patient.gender,
273
+ patient_identifier.identifier AS arv_number
274
+ FROM temp_earliest_start_date AS patient
275
+ INNER JOIN temp_patient_outcomes AS outcomes
276
+ ON outcomes.patient_id = patient.patient_id
277
+ AND outcomes.cum_outcome = 'On antiretrovirals'
278
+ INNER JOIN patient_identifier
279
+ ON patient_identifier.patient_id = patient.patient_id
280
+ AND patient_identifier.identifier_type IN (#{pepfar_patient_identifier_type.to_sql})
281
+ AND patient_identifier.voided = 0
282
+ WHERE patient.patient_id NOT IN (
283
+ SELECT DISTINCT orders.patient_id FROM orders
284
+ INNER JOIN order_type ON order_type.order_type_id = orders.order_type_id AND order_type.name = 'Lab'
285
+ INNER JOIN obs ON orders.order_id = obs.order_id AND obs.voided = 0
286
+ INNER JOIN concept_name ON concept_name.concept_id = obs.concept_id AND concept_name.name = 'Test type' AND concept_name.voided = 0
287
+ INNER JOIN concept_name AS test_name ON test_name.concept_id = obs.value_coded AND test_name.name = 'HIV Viral Load' AND test_name.voided = 0
288
+ WHERE orders.start_date <= DATE(#{ActiveRecord::Base.connection.quote(end_date)}) - INTERVAL 12 MONTH
289
+ AND orders.concept_id IN (SELECT concept_id FROM concept_name WHERE name IN ('Blood', 'DBS (Free drop to DBS card)', 'DBS (Using capillary tube)'))
290
+ AND orders.voided = 0
291
+ ) AND patient.earliest_start_date <= DATE(#{ActiveRecord::Base.connection.quote(end_date)}) - INTERVAL 6 MONTH
292
+ GROUP BY patient.patient_id
293
+ SQL
294
+ end
295
+
296
+ ##
297
+ # Find all patients that are on treatment with at least one VL before end of reporting period.
298
+ def find_patients_with_viral_load
299
+ ActiveRecord::Base.connection.select_all <<~SQL
300
+ SELECT orders.patient_id,
301
+ disaggregated_age_group(patient.birthdate,
302
+ DATE(#{ActiveRecord::Base.connection.quote(end_date)})) AS age_group,
303
+ patient.birthdate,
304
+ patient.gender,
305
+ patient_identifier.identifier AS arv_number,
306
+ orders.start_date AS order_date,
307
+ COALESCE(orders.discontinued_date, orders.start_date) AS sample_draw_date,
308
+ COALESCE(reason_for_test_value.name, reason_for_test.value_text) AS reason_for_test,
309
+ result.value_modifier AS result_modifier,
310
+ COALESCE(result.value_numeric, result.value_text) AS result_value
311
+ FROM orders
312
+ INNER JOIN order_type
313
+ ON order_type.order_type_id = orders.order_type_id
314
+ AND order_type.name = 'Lab'
315
+ AND order_type.retired = 0
316
+ INNER JOIN concept_name
317
+ ON concept_name.concept_id = orders.concept_id
318
+ AND concept_name.name IN ('Blood', 'DBS (Free drop to DBS card)', 'DBS (Using capillary tube)')
319
+ AND concept_name.voided = 0
320
+ LEFT JOIN obs AS reason_for_test
321
+ ON reason_for_test.order_id = orders.order_id
322
+ AND reason_for_test.concept_id IN (SELECT concept_id FROM concept_name WHERE name LIKE 'Reason for test' AND voided = 0)
323
+ AND reason_for_test.voided = 0
324
+ LEFT JOIN concept_name AS reason_for_test_value
325
+ ON reason_for_test_value.concept_id = reason_for_test.value_coded
326
+ AND reason_for_test_value.voided = 0
327
+ LEFT JOIN obs AS result
328
+ ON result.order_id = orders.order_id
329
+ AND result.concept_id IN (SELECT concept_id FROM concept_name WHERE name LIKE 'HIV Viral load' AND voided = 0)
330
+ AND result.voided = 0
331
+ AND (result.value_text IS NOT NULL OR result.value_numeric IS NOT NULL)
332
+ INNER JOIN (
333
+ /* Get the latest order dates for each patient */
334
+ SELECT orders.patient_id, MAX(orders.start_date) AS start_date
335
+ FROM orders
336
+ INNER JOIN order_type
337
+ ON order_type.order_type_id = orders.order_type_id
338
+ AND order_type.name = 'Lab'
339
+ AND order_type.retired = 0
340
+ INNER JOIN concept_name
341
+ ON concept_name.concept_id = orders.concept_id
342
+ AND concept_name.name IN ('Blood', 'DBS (Free drop to DBS card)', 'DBS (Using capillary tube)')
343
+ AND concept_name.voided = 0
344
+ WHERE orders.start_date < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
345
+ AND orders.voided = 0
346
+ GROUP BY orders.patient_id
347
+ ) AS latest_patient_order_date
348
+ ON latest_patient_order_date.patient_id = orders.patient_id
349
+ AND latest_patient_order_date.start_date = orders.start_date
350
+ INNER JOIN temp_earliest_start_date AS patient ON patient.patient_id = orders.patient_id
351
+ INNER JOIN temp_patient_outcomes AS outcomes
352
+ ON outcomes.patient_id = patient.patient_id
353
+ AND outcomes.cum_outcome = 'On antiretrovirals'
354
+ LEFT JOIN patient_identifier
355
+ ON patient_identifier.patient_id = orders.patient_id
356
+ AND patient_identifier.identifier_type IN (#{pepfar_patient_identifier_type.to_sql})
357
+ AND patient_identifier.voided = 0
358
+ WHERE orders.start_date < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
359
+ AND orders.start_date >= DATE(#{ActiveRecord::Base.connection.quote(start_date)})
360
+ GROUP BY orders.patient_id
361
+ SQL
362
+ end
363
+
364
+ ##
365
+ # Returns a Relation of all viral load tests.
366
+ def find_viral_load_tests
367
+ ::Lab::LabTest.where(value_coded: concept('Viral load'))
368
+ end
369
+
370
+ def find_emastercard_patient_results
371
+ ActiveRecord::Base.connection.select_all <<~SQL
372
+ SELECT obs.person_id AS patient_id,
373
+ patient_identifier.identifier AS arv_number,
374
+ patient.birthdate,
375
+ patient.gender,
376
+ disaggregated_age_group(patient.birthdate, #{ActiveRecord::Base.connection.quote(end_date)}) AS age_group,
377
+ obs.value_numeric AS result_value
378
+ FROM obs
379
+ INNER JOIN encounter ON encounter.encounter_id = obs.encounter_id AND encounter.voided = 0
380
+ INNER JOIN encounter_type ON encounter_type.encounter_type_id = encounter.encounter_type AND encounter_type.name = 'Lab'
381
+ INNER JOIN temp_earliest_start_date AS patient ON patient.patient_id = obs.person_id
382
+ INNER JOIN temp_patient_outcomes
383
+ ON temp_patient_outcomes.patient_id = obs.person_id
384
+ AND temp_patient_outcomes.cum_outcome = 'On antiretrovirals'
385
+ LEFT JOIN patient_identifier
386
+ ON patient_identifier.patient_id = obs.person_id
387
+ AND patient_identifier.voided = 0
388
+ AND patient_identifier.identifier_type IN (#{pepfar_patient_identifier_type.to_sql})
389
+ INNER JOIN (
390
+ SELECT obs.person_id, MAX(obs.obs_datetime) AS obs_datetime
391
+ FROM obs
392
+ INNER JOIN encounter ON encounter.encounter_id = obs.encounter_id AND encounter.voided = 0
393
+ INNER JOIN encounter_type ON encounter_type.encounter_type_id = encounter.encounter_type AND encounter_type.name = 'Lab'
394
+ WHERE obs.concept_id IN (#{concept('Viral load').to_sql})
395
+ AND obs.obs_datetime > DATE(#{ActiveRecord::Base.connection.quote(start_date)}) - INTERVAL 1 DAY
396
+ AND obs.obs_datetime < DATE(#{ActiveRecord::Base.connection.quote(end_date)}) + INTERVAL 1 DAY
397
+ AND obs.voided = 0
398
+ GROUP BY obs.person_id
399
+ ) AS latest_results
400
+ ON latest_results.person_id = obs.person_id
401
+ AND latest_results.obs_datetime = obs.obs_datetime
402
+ WHERE obs.concept_id IN (#{concept('Viral load').to_sql})
403
+ AND obs.value_numeric IS NOT NULL
404
+ AND obs.voided = 0
405
+ GROUP BY obs.person_id
406
+ SQL
407
+ end
408
+
409
+ def concept(name)
410
+ ::ConceptName.where(name:).select(:concept_id)
411
+ end
412
+ end
413
+ end
414
+ end