malawi_hiv_program_reports 1.0.1 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/services/malawi_hiv_program_reports/README.md +16 -0
- data/app/services/malawi_hiv_program_reports/adapters/moh/custom.rb +199 -0
- data/app/services/malawi_hiv_program_reports/archiving_candidates.rb +130 -0
- data/app/services/malawi_hiv_program_reports/arv_refill_periods.rb +311 -0
- data/app/services/malawi_hiv_program_reports/clinic/README.md +5 -0
- data/app/services/malawi_hiv_program_reports/clinic/appointments_report.rb +317 -0
- data/app/services/malawi_hiv_program_reports/clinic/discrepancy_report.rb +42 -0
- data/app/services/malawi_hiv_program_reports/clinic/docs/hypertension_report.md +31 -0
- data/app/services/malawi_hiv_program_reports/clinic/drug_dispensations.rb +48 -0
- data/app/services/malawi_hiv_program_reports/clinic/external_consultation_clients.rb +69 -0
- data/app/services/malawi_hiv_program_reports/clinic/hypertension_report.rb +223 -0
- data/app/services/malawi_hiv_program_reports/clinic/ipt_coverage.rb +112 -0
- data/app/services/malawi_hiv_program_reports/clinic/ipt_report.rb +69 -0
- data/app/services/malawi_hiv_program_reports/clinic/lims_results.rb +55 -0
- data/app/services/malawi_hiv_program_reports/clinic/outcome_list.rb +127 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_alive_and_on_treatment.rb +57 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_due_for_viral_load.rb +39 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_on_antiretrovirals.rb +44 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_on_dtg.rb +36 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_on_treatment.rb +42 -0
- data/app/services/malawi_hiv_program_reports/clinic/patients_with_outdated_demographics.rb +173 -0
- data/app/services/malawi_hiv_program_reports/clinic/pregnant_patients.rb +91 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimen_dispensation_data.rb +282 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimen_switch.rb +456 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimens_and_formulations.rb +182 -0
- data/app/services/malawi_hiv_program_reports/clinic/regimens_by_weight_and_gender.rb +108 -0
- data/app/services/malawi_hiv_program_reports/clinic/retention.rb +246 -0
- data/app/services/malawi_hiv_program_reports/clinic/stock_card_report.rb +65 -0
- data/app/services/malawi_hiv_program_reports/clinic/tpt_outcome.rb +494 -0
- data/app/services/malawi_hiv_program_reports/clinic/tx_rtt.rb +169 -0
- data/app/services/malawi_hiv_program_reports/clinic/viral_load.rb +292 -0
- data/app/services/malawi_hiv_program_reports/clinic/viral_load_disaggregated.rb +97 -0
- data/app/services/malawi_hiv_program_reports/clinic/viral_load_results.rb +175 -0
- data/app/services/malawi_hiv_program_reports/clinic/visits_report.rb +113 -0
- data/app/services/malawi_hiv_program_reports/clinic/vl_collection.rb +48 -0
- data/app/services/malawi_hiv_program_reports/cohort/outcomes.rb +338 -0
- data/app/services/malawi_hiv_program_reports/cohort/regimens.rb +69 -0
- data/app/services/malawi_hiv_program_reports/cohort/side_effects.rb +141 -0
- data/app/services/malawi_hiv_program_reports/cohort/tpt.rb +172 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort.rb +278 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_builder.rb +2337 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated.rb +608 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_additions.rb +208 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_builder.rb +526 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_struct.rb +219 -0
- data/app/services/malawi_hiv_program_reports/moh/cohort_survival_analysis.rb +203 -0
- data/app/services/malawi_hiv_program_reports/moh/moh_tpt.rb +223 -0
- data/app/services/malawi_hiv_program_reports/moh/tpt_newly_initiated.rb +235 -0
- data/app/services/malawi_hiv_program_reports/pepfar/defaulter_list.rb +25 -0
- data/app/services/malawi_hiv_program_reports/pepfar/maternal_status.rb +29 -0
- data/app/services/malawi_hiv_program_reports/pepfar/patient_start_vl.rb +45 -0
- data/app/services/malawi_hiv_program_reports/pepfar/regimen_switch.rb +479 -0
- data/app/services/malawi_hiv_program_reports/pepfar/sc_arvdisp.rb +174 -0
- data/app/services/malawi_hiv_program_reports/pepfar/sc_curr.rb +98 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tb_prev.rb +163 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tb_prev2.rb +222 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tb_prev3.rb +421 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tpt_status.rb +181 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_ml.rb +181 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_new.rb +205 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_rtt.rb +288 -0
- data/app/services/malawi_hiv_program_reports/pepfar/tx_tb.rb +283 -0
- data/app/services/malawi_hiv_program_reports/pepfar/utils.rb +141 -0
- data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage.rb +414 -0
- data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage2.rb +433 -0
- data/app/services/malawi_hiv_program_reports/report_map.rb +56 -0
- data/app/services/malawi_hiv_program_reports/utils/README.md +8 -0
- data/app/services/malawi_hiv_program_reports/utils/common_sql_query_utils.rb +60 -0
- data/app/services/malawi_hiv_program_reports/utils/concurrency_utils.rb +53 -0
- data/app/services/malawi_hiv_program_reports/utils/docs/common_sql_query_utils.md +53 -0
- data/app/services/malawi_hiv_program_reports/utils/model_utils.rb +66 -0
- data/app/services/malawi_hiv_program_reports/utils/parameter_utils.rb +32 -0
- data/app/services/malawi_hiv_program_reports/utils/time_utils.rb +52 -0
- data/lib/malawi_hiv_program_reports/version.rb +1 -1
- 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
|