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