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,421 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Pepfar
5
+ ##
6
+ # Patients who started TPT just before the start of the current
7
+ # and have finished within the current reporting period.
8
+ class TbPrev3
9
+
10
+ attr_reader :start_date, :end_date, :check_date, :cut_off_point, :occupation, :location
11
+
12
+ include MalawiHivProgramReports::Adapters::Moh::Custom
13
+ include Utils
14
+ include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
15
+
16
+ def initialize(start_date:, end_date:, **kwargs)
17
+ @start_date = ActiveRecord::Base.connection.quote(start_date)
18
+ @check_date = start_date.to_date - 6.months
19
+ @cut_off_point = start_date.to_date
20
+ @end_date = ActiveRecord::Base.connection.quote(end_date)
21
+ @occupation = kwargs[:occupation]
22
+ @location = kwargs[:location]
23
+ end
24
+
25
+ def find_report
26
+ report = init_report
27
+ patients = group_patients_by_tpt_course(patients_on_tpt)
28
+
29
+ load_patients_into_report(report, patients.six_h, '6H') do |patient|
30
+ # 6H has a constant dosage of 1 pill per day
31
+ patient_completed_tpt?(patient, '6H')
32
+ end
33
+
34
+ load_patients_into_report(report, patients.three_hp, '3HP') do |patient|
35
+ # 3HP daily dosages vary by patient weight can't use easily use pills
36
+ # to determine course completion
37
+ patient_completed_tpt?(patient, '3HP')
38
+ end
39
+
40
+ report
41
+ end
42
+
43
+ def fetch_individual_report(patient_id)
44
+ individual_tpt_report(patient_id)
45
+ end
46
+
47
+ private
48
+
49
+ def init_report
50
+ pepfar_age_groups.each_with_object({}) do |age_group, report|
51
+ report[age_group] = %w[M F Unknown].each_with_object({}) do |gender, gender_sub_report|
52
+ gender_sub_report[gender] = %w[6H 3HP].each_with_object({}) do |tpt, tpt_sub_report|
53
+ tpt_sub_report[tpt] = {
54
+ started_new_on_art: [],
55
+ started_previously_on_art: [],
56
+ completed_new_on_art: [],
57
+ completed_previously_on_art: []
58
+ }
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def load_patients_into_report(report, patients, tpt, &patient_has_completed_tpt)
65
+ patients.each do |patient|
66
+ next if patient['transfer_in'] == 1 && !patient_has_completed_tpt[patient]
67
+
68
+ age_group = patient['age_group']
69
+ gender = patient['gender']&.first&.upcase || 'Unknown'
70
+ tpt_states = find_patient_tpt_state(patient, &patient_has_completed_tpt)
71
+
72
+ tpt_states.each do |tpt_state|
73
+ report[age_group][gender][tpt][tpt_state] << patient
74
+ end
75
+ end
76
+ end
77
+
78
+ def find_patient_tpt_state(patient, &patient_has_completed_tpt)
79
+ if patient_has_completed_tpt[patient]
80
+ return %i[completed_new_on_art] if patient_new_on_art?(patient) && patient['transfer_in'] == 1
81
+
82
+ return %i[started_new_on_art completed_new_on_art] if patient_new_on_art?(patient)
83
+
84
+ return %i[completed_previously_on_art] if patient['transfer_in'] == 1
85
+
86
+ return %i[started_previously_on_art completed_previously_on_art]
87
+ end
88
+
89
+ return %i[started_new_on_art] if patient_new_on_art?(patient)
90
+
91
+ %i[started_previously_on_art]
92
+ end
93
+
94
+ def patient_new_on_art?(patient)
95
+ tpt_initiation_date = patient['tpt_initiation_date'].to_date
96
+ art_start_date = patient['art_start_date'].to_date
97
+
98
+ (tpt_initiation_date >= art_start_date) && (tpt_initiation_date < art_start_date + 180.days)
99
+ end
100
+
101
+ def patients_on_tpt
102
+ clients = fetch_patients_on_tpt.to_a
103
+ results = []
104
+ clients.each do |client|
105
+ next if client['tpt_initiation_date'].to_date > cut_off_point
106
+
107
+ result = individual_tpt_report(client['patient_id'])
108
+ next if result.blank?
109
+ next if result['tpt_initiation_date'].to_date < check_date
110
+
111
+ client['tpt_initiation_date'] = result['tpt_initiation_date']
112
+ client['total_pills_taken'] = result['total_pills_taken']
113
+ client['months_on_tpt'] = result['months_on_tpt']
114
+ client['total_days_on_medication'] = result['total_days_on_medication']
115
+ client['drug_concepts'] = result['drug_concepts']
116
+ client['transfer_in'] = result['transfer_in']
117
+ results << client
118
+ end
119
+ results
120
+ end
121
+
122
+ def fetch_patients_on_tpt
123
+ ActiveRecord::Base.connection.select_all <<~SQL
124
+ SELECT person.person_id AS patient_id,
125
+ patient_identifier.identifier AS arv_number,
126
+ DATE(MIN(orders.start_date)) AS tpt_initiation_date,
127
+ #{function_manager(function: 'date_antiretrovirals_started', location: @location, args: "person.person_id::int, MIN(denominator_patient.start_date)::date, #{@location}::int")} AS art_start_date,
128
+ #{function_manager(function: 'patient_outcome', location: @location, args: "person.person_id::int, #{@end_date}::date, #{@location}::int")} AS outcome,
129
+ person.gender,
130
+ person.birthdate,
131
+ #{function_manager(function: 'disaggregated_age_group', location: @location, args: "person.birthdate::date, #{@end_date}::date")} AS age_group
132
+ FROM person
133
+ LEFT JOIN patient_identifier
134
+ ON patient_identifier.patient_id = person.person_id
135
+ AND patient_identifier.voided = 0
136
+ AND #{in_manager(column: 'patient_identifier.identifier_type', values: "(SELECT patient_identifier_type_id FROM patient_identifier_type WHERE name = 'ARV Number')")}
137
+ #{site_manager(operator: 'AND', column: 'patient_identifier.site_id', location: @location)}
138
+ LEFT JOIN (#{current_occupation_query}) AS current_occupation ON current_occupation.person_id = person.person_id
139
+ #{site_manager(operator: 'AND', column: 'current_occupation.site_id', location: @location)}
140
+ INNER JOIN(
141
+ SELECT denominator_encounter.patient_id AS patient_id, patient_state.start_date AS start_date
142
+ FROM person
143
+ INNER JOIN patient_program
144
+ ON patient_program.patient_id = person.person_id
145
+ AND #{in_manager(column: 'patient_program.program_id', values: "(SELECT program_id FROM program WHERE name = 'HIV PROGRAM')")}
146
+ AND patient_program.voided = 0
147
+ INNER JOIN patient_state
148
+ ON patient_state.patient_program_id = patient_program.patient_program_id
149
+ AND patient_state.state = 7 /* State: 7 == On antiretrovirals */
150
+ AND patient_state.start_date < DATE(#{start_date})
151
+ AND patient_state.voided = 0
152
+ INNER JOIN encounter AS denominator_encounter
153
+ ON denominator_encounter.patient_id = patient_program.patient_id
154
+ AND #{in_manager(column: 'denominator_encounter.program_id', values: "(SELECT program_id FROM program WHERE name = 'HIV PROGRAM')")}
155
+ AND #{in_manager(column: 'denominator_encounter.encounter_type', values: "(SELECT encounter_type_id FROM encounter_type WHERE name = 'Treatment')")}
156
+ AND denominator_encounter.encounter_datetime >= #{interval_manager(date: start_date, value: 6, interval: 'MONTH', operator: '-')}
157
+ AND denominator_encounter.encounter_datetime <= DATE(#{start_date})
158
+ AND denominator_encounter.voided = 0
159
+ #{site_manager(operator: 'AND', column: 'patient_program.site_id', location: @location)}
160
+ GROUP BY denominator_encounter.patient_id, patient_state.start_date
161
+ ) AS denominator_patient ON denominator_patient.patient_id = person.person_id
162
+ INNER JOIN encounter AS prescription_encounter
163
+ ON prescription_encounter.patient_id = denominator_patient.patient_id
164
+ AND prescription_encounter.program_id = 1
165
+ AND #{in_manager(column: 'prescription_encounter.encounter_type', values: "(SELECT encounter_type_id FROM encounter_type WHERE name = 'Treatment')")}
166
+ AND prescription_encounter.encounter_datetime >= #{interval_manager(date: start_date, value: 6, interval: 'MONTH', operator: '-')}
167
+ AND prescription_encounter.encounter_datetime <= DATE(#{end_date})
168
+ AND prescription_encounter.voided = 0
169
+ INNER JOIN orders
170
+ ON orders.encounter_id = prescription_encounter.encounter_id
171
+ AND #{in_manager(column: 'orders.order_type_id', values: "(SELECT order_type_id FROM order_type WHERE name = 'Drug order')")}
172
+ AND orders.start_date >= #{interval_manager(date: start_date, value: 6, interval: 'MONTH', operator: '-')}
173
+ AND orders.start_date <= CAST(#{end_date} as DATE)
174
+ AND orders.voided = 0
175
+ INNER JOIN concept_name
176
+ ON concept_name.concept_id = orders.concept_id
177
+ AND #{in_manager(column: 'concept_name.name', values: "'Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine'" )}
178
+ INNER JOIN drug_order
179
+ ON drug_order.order_id = orders.order_id
180
+ AND drug_order.quantity > 0
181
+ WHERE person.voided = 0 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'current_occupation', include_clause: false)}
182
+ #{site_manager(operator: 'AND', column: 'person.site_id', location: @location)}
183
+ AND person.person_id NOT IN (
184
+ /* External consultations */
185
+ SELECT DISTINCT registration_encounter.patient_id
186
+ FROM patient_program pp
187
+ INNER JOIN program p ON p.program_id = pp.program_id AND p.name = 'HIV PROGRAM' AND p.retired = 0
188
+ INNER JOIN encounter AS registration_encounter
189
+ ON registration_encounter.patient_id = pp.patient_id
190
+ AND registration_encounter.program_id = pp.program_id
191
+ AND registration_encounter.encounter_datetime < #{interval_manager(date: end_date, value: 1, interval: 'MONTH', operator: '+')}
192
+ AND registration_encounter.voided = 0
193
+ INNER JOIN (
194
+ SELECT MAX(encounter.encounter_datetime) AS encounter_datetime, encounter.patient_id
195
+ FROM encounter
196
+ INNER JOIN encounter_type
197
+ ON encounter_type.encounter_type_id = encounter.encounter_type
198
+ AND encounter_type.name = 'Registration'
199
+ INNER JOIN program
200
+ ON program.program_id = encounter.program_id
201
+ AND program.name = 'HIV PROGRAM'
202
+ WHERE encounter.encounter_datetime < CAST(#{end_date} AS DATE) AND encounter.voided = 0
203
+ GROUP BY encounter.patient_id
204
+ ) AS max_registration_encounter
205
+ ON max_registration_encounter.patient_id = registration_encounter.patient_id
206
+ AND max_registration_encounter.encounter_datetime = registration_encounter.encounter_datetime
207
+ INNER JOIN obs AS patient_type_obs
208
+ ON patient_type_obs.encounter_id = registration_encounter.encounter_id
209
+ AND #{in_manager(column: 'patient_type_obs.concept_id', values: "(SELECT concept_id FROM concept_name WHERE name = 'Type of patient' AND voided = 0)")}
210
+ AND #{in_manager(column: 'patient_type_obs.value_coded', values: "(SELECT concept_id FROM concept_name WHERE name IN ('Drug refill', 'External consultation') AND voided = 0)")}
211
+ AND patient_type_obs.voided = 0
212
+ WHERE pp.voided = 0
213
+ )
214
+ GROUP BY person.person_id #{group_by_columns('patient_identifier.identifier, person.gender, person.birthdate, denominator_patient.start_date')}
215
+ SQL
216
+ end
217
+
218
+ def individual_tpt_report(patient_id)
219
+ result = process_current_tpt_course_date(patient_id)
220
+ c_start_date = ActiveRecord::Base.connection.quote(result[:start_date])
221
+ c_end_date = ActiveRecord::Base.connection.quote(client_tpt_end_date(patient_id, c_start_date))
222
+ ActiveRecord::Base.connection.select_one <<-SQL
223
+ SELECT
224
+ CASE
225
+ WHEN tpt_transfer_in_obs.value_datetime IS NULL THEN DATE(MIN(o.start_date))
226
+ WHEN tpt_transfer_in_obs.value_datetime > MIN(o.start_date) THEN DATE(MIN(o.start_date))
227
+ ELSE DATE(tpt_transfer_in_obs.value_datetime)
228
+ END AS tpt_initiation_date,
229
+ COUNT(DISTINCT(DATE(o.start_date))) AS months_on_tpt,
230
+ SUM(dor.quantity) + SUM(CASE WHEN tpt_transfer_in_obs.value_numeric IS NOT NULL THEN tpt_transfer_in_obs.value_numeric ELSE 0 END) AS total_pills_taken,
231
+ SUM(DATEDIFF(o.auto_expire_date, o.start_date)) + SUM(CASE WHEN tpt_transfer_in_obs.value_datetime IS NOT NULL THEN DATEDIFF(tpt_transfer_in_obs.obs_datetime, tpt_transfer_in_obs.value_datetime) ElSE 0 END) AS total_days_on_medication,
232
+ GROUP_CONCAT(DISTINCT o.concept_id SEPARATOR ',') AS drug_concepts,
233
+ CASE
234
+ WHEN tpt_transfer_in_obs.value_numeric IS NOT NULL THEN 1
235
+ ELSE 0
236
+ END AS transfer_in,
237
+ MAX(o.start_date) AS last_dispensed_date,
238
+ MAX(o.auto_expire_date) AS auto_expire_date
239
+ FROM orders o
240
+ INNER JOIN concept_name cn
241
+ ON cn.concept_id = o.concept_id
242
+ AND cn.name IN ('Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine')
243
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
244
+ LEFT JOIN obs tpt_transfer_in_obs
245
+ ON tpt_transfer_in_obs.person_id = o.patient_id
246
+ AND tpt_transfer_in_obs.concept_id = #{::ConceptName.find_by_name('TPT Drugs Received').concept_id}
247
+ AND tpt_transfer_in_obs.voided = 0
248
+ #{site_manager(operator: 'AND', column: 'tpt_transfer_in_obs.site_id', location: @location)}
249
+ AND tpt_transfer_in_obs.value_drug IN (SELECT drug_id FROM drug WHERE concept_id IN (SELECT concept_id FROM concept_name WHERE name IN ('Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine')))
250
+ INNER JOIN drug_order dor
251
+ ON dor.order_id = o.order_id
252
+ AND dor.quantity > 0
253
+ WHERE DATE(o.start_date) BETWEEN DATE(#{c_start_date}) AND DATE(#{c_end_date})
254
+ AND o.order_type_id IN (SELECT order_type_id FROM order_type WHERE name = 'Drug order')
255
+ AND o.voided = 0
256
+ AND o.patient_id = #{patient_id}
257
+ GROUP BY o.patient_id
258
+ SQL
259
+ end
260
+
261
+ def process_current_tpt_course_date(patient_id)
262
+ result = client_tpt_dates(patient_id)
263
+ return { start_date: '1900-01-01', end_date: } if result.blank?
264
+
265
+ sorted_result = result.sort { |a, b| a['start_date'].to_date <=> b['start_date'].to_date }.reverse
266
+ return_date = { start_date: sorted_result.last['start_date'], end_date: }
267
+
268
+ course_interruption = result.first['course'] == '3HP' ? 1 : 2
269
+ # loop through the result array and find the first gap in the dates that equals the course interruption
270
+ sorted_result.each_with_index do |row, index|
271
+ next if index.zero?
272
+
273
+ diff = ActiveRecord::Base.connection.select_one("SELECT TIMESTAMPDIFF(MONTH,DATE('#{row['end_date']}'), DATE('#{sorted_result[index - 1]['start_date']}')) as months")['months']
274
+
275
+ if diff.to_i >= course_interruption
276
+ return_date = { start_date: sorted_result[index - 1]['start_date'], end_date: }
277
+ break
278
+ end
279
+ end
280
+ return_date
281
+ end
282
+
283
+ def client_tpt_dates(patient_id)
284
+ ActiveRecord::Base.connection.select_all <<~SQL
285
+ (
286
+ SELECT
287
+ DATE(o.value_datetime) AS start_date,
288
+ DATE(o.obs_datetime) AS end_date,
289
+ CASE
290
+ WHEN count(distinct(o.value_drug)) > 1 THEN '3HP'
291
+ WHEN o.value_drug = #{isoniazid_rifapentine_drug.drug_id} THEN '3HP'
292
+ ELSE '6H'
293
+ END AS course
294
+ FROM obs o
295
+ WHERE o.concept_id = #{::ConceptName.find_by_name('TPT Drugs Received').concept_id}
296
+ AND o.voided = 0
297
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
298
+ AND o.value_drug IN (SELECT drug_id FROM drug WHERE concept_id IN (SELECT concept_id FROM concept_name WHERE name IN ('Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine')))
299
+ AND o.person_id = #{patient_id}
300
+ AND o.value_numeric IS NOT NULL
301
+ AND DATE(o.obs_datetime) <= DATE(#{start_date})
302
+ GROUP BY DATE(o.obs_datetime)
303
+ ORDER BY DATE(o.obs_datetime) DESC
304
+ )
305
+ UNION
306
+ (
307
+ SELECT
308
+ DATE(o.start_date) AS start_date,
309
+ DATE(o.auto_expire_date) AS end_date,
310
+ CASE
311
+ WHEN count(distinct(o.concept_id)) > 1 THEN '3HP'
312
+ WHEN o.concept_id = #{isoniazid_rifapentine_concept.concept_id} THEN '3HP'
313
+ ELSE '6H'
314
+ END AS course
315
+ FROM orders o
316
+ INNER JOIN encounter e ON e.encounter_id = o.encounter_id AND e.voided = 0 AND e.program_id = 1 /* HIV PROGRAM */
317
+ INNER JOIN drug_order dor ON dor.order_id = o.order_id AND dor.quantity > 0
318
+ WHERE o.order_type_id IN (SELECT order_type_id FROM order_type WHERE name = 'Drug order')
319
+ AND o.voided = 0
320
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
321
+ AND o.concept_id IN (#{::ConceptName.where(name: ['Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine']).select(:concept_id).to_sql})
322
+ AND o.patient_id = #{patient_id}
323
+ AND o.auto_expire_date IS NOT NULL
324
+ AND DATE(o.start_date) <= DATE(#{start_date})
325
+ GROUP BY DATE(o.start_date)
326
+ ORDER BY DATE(o.start_date) DESC
327
+ )
328
+ SQL
329
+ end
330
+
331
+ def client_tpt_end_date(patient_id, start_date)
332
+ # Get patient tpt dispensations dates after the start date and before the end date
333
+ result = ActiveRecord::Base.connection.select_all <<~SQL
334
+ (
335
+ SELECT
336
+ DATE(o.value_datetime) AS start_date,
337
+ DATE(MAX(o.obs_datetime)) AS end_date,
338
+ CASE
339
+ WHEN count(distinct(o.value_drug)) > 1 THEN '3HP'
340
+ WHEN o.value_drug = #{isoniazid_rifapentine_drug.drug_id} THEN '3HP'
341
+ ELSE '6H'
342
+ END AS course
343
+ FROM obs o
344
+ WHERE o.concept_id = #{::ConceptName.find_by_name('TPT Drugs Received').concept_id}
345
+ AND o.voided = 0
346
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
347
+ AND o.value_drug IN (SELECT drug_id FROM drug WHERE concept_id IN (SELECT concept_id FROM concept_name WHERE name IN ('Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine')))
348
+ AND o.person_id = #{patient_id}
349
+ AND o.value_numeric IS NOT NULL
350
+ AND DATE(o.obs_datetime) BETWEEN DATE(#{start_date}) AND DATE(#{end_date})
351
+ GROUP BY DATE(o.obs_datetime)
352
+ ORDER BY DATE(o.obs_datetime) DESC
353
+ )
354
+ UNION
355
+ (
356
+ SELECT
357
+ DATE(o.start_date) AS start_date,
358
+ DATE(MAX(o.auto_expire_date)) AS end_date,
359
+ CASE
360
+ WHEN count(distinct(o.concept_id)) > 1 THEN '3HP'
361
+ WHEN o.concept_id = #{isoniazid_rifapentine_concept.concept_id} THEN '3HP'
362
+ ELSE '6H'
363
+ END AS course
364
+ FROM orders o
365
+ INNER JOIN encounter e ON e.encounter_id = o.encounter_id AND e.voided = 0 AND e.program_id = 1 /* HIV PROGRAM */
366
+ INNER JOIN drug_order dor ON dor.order_id = o.order_id AND dor.quantity > 0
367
+ WHERE o.order_type_id IN (SELECT order_type_id FROM order_type WHERE name = 'Drug order')
368
+ AND o.voided = 0
369
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
370
+ AND o.concept_id IN (#{::ConceptName.where(name: ['Rifapentine', 'Isoniazid', 'Isoniazid/Rifapentine']).select(:concept_id).to_sql})
371
+ AND o.patient_id = #{patient_id}
372
+ AND o.auto_expire_date IS NOT NULL
373
+ AND DATE(o.start_date) BETWEEN DATE(#{start_date}) AND DATE(#{end_date})
374
+ GROUP BY DATE(o.start_date)
375
+ ORDER BY DATE(o.start_date) DESC
376
+ )
377
+ SQL
378
+
379
+ return end_date if result.blank?
380
+
381
+ sorted_result = result.sort { |a, b| a['start_date'].to_date <=> b['start_date'].to_date }
382
+ return_date = sorted_result.last['end_date']
383
+ course_interruption = result.first['course'] == '3HP' ? 1 : 2
384
+ # use a for loop to check if there is a course interruption
385
+ sorted_result.each_with_index do |row, i|
386
+ next if i.zero?
387
+
388
+ if row['course'] != sorted_result[i - 1]['course']
389
+ return_date = sorted_result[i - 1]['end_date']
390
+ break
391
+ end
392
+ diff = ActiveRecord::Base.connection.select_one("SELECT TIMESTAMPDIFF(MONTH,DATE('#{sorted_result[i - 1]['end_date']}'),DATE('#{row['start_date']}')) as months")['months']
393
+ if diff.to_i >= course_interruption
394
+ return_date = sorted_result[i - 1]['end_date']
395
+ break
396
+ end
397
+ end
398
+ return_date
399
+ end
400
+
401
+ ##
402
+ # Groups patients into their TPT categories (ie 6H and 3HP) based on their drugs
403
+ #
404
+ # Returns an object with a three_hp and six_h methods, each of which
405
+ # is an array of patients for that category.
406
+ def group_patients_by_tpt_course(patients)
407
+ patients.each_with_object(OpenStruct.new(six_h: [], three_hp: [])) do |patient, categories|
408
+ if patient_on_3hp?(patient)
409
+ categories.three_hp << patient
410
+ else
411
+ categories.six_h << patient
412
+ end
413
+ end
414
+ end
415
+
416
+ def isoniazid_rifapentine_drug
417
+ @isoniazid_rifapentine_drug ||= ::Drug.find_by!(concept_id: isoniazid_rifapentine_concept.concept_id)
418
+ end
419
+ end
420
+ end
421
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Pepfar
5
+ # This class is used to generate the TPT Status report for an ART patient
6
+ class TptStatus
7
+ attr_reader :start_date, :end_date, :patient_id
8
+
9
+ include Utils
10
+
11
+ def initialize(start_date:, end_date:, **kwargs)
12
+ @start_date = start_date
13
+ @end_date = end_date
14
+ @patient_id = kwargs[:patient_id]
15
+ @tpt_status = {}
16
+ end
17
+
18
+ def find_report
19
+ patient_tpt_status
20
+ rescue StandardError => e
21
+ Rails.logger.error("Error generating TPT Status report for patient #{patient_id}: #{e.message}")
22
+ Rails.logger.error(e.backtrace.join("\n"))
23
+ raise e
24
+ end
25
+
26
+ private
27
+
28
+ def patient_tpt_status
29
+ return tb_treatment_status if patient_on_tb_treatment?(patient_id)
30
+ return completed_tpt_status if patient_history_on_completed_tpt
31
+
32
+ patient = TbPrev3.new(start_date:, end_date:).fetch_individual_report(patient_id)
33
+ return default_status if patient.blank?
34
+
35
+ tpt_status_based_on_patient(patient)
36
+ end
37
+
38
+ def tb_treatment_status
39
+ {
40
+ tpt: nil,
41
+ completed: false,
42
+ tb_treatment: true,
43
+ tpt_init_date: nil,
44
+ tpt_complete_date: nil,
45
+ eligible: {
46
+ '3HP': false,
47
+ '6H': false
48
+ }
49
+ }
50
+ end
51
+
52
+ def completed_tpt_status
53
+ tpt = patient_history_on_completed_tpt.include?('IPT') ? '6H' : '3HP'
54
+ { tpt:, completed: true, tb_treatment: false, tpt_init_date: nil, tpt_complete_date: nil, tpt_end_date: nil,
55
+ eligible: {
56
+ '3HP': false,
57
+ '6H': false
58
+ } }
59
+ end
60
+
61
+ def default_status
62
+ patient = ::Patient.find(patient_id)
63
+ art_start_date = patient.art_start_date
64
+ { tpt: nil, completed: false, tb_treatment: false, tpt_init_date: nil, tpt_complete_date: nil, tpt_end_date: nil,
65
+ eligible: {
66
+ '3HP': art_start_date ? difference_in_months(end_date.to_date, art_start_date.to_date) < 3 : true,
67
+ '6H': art_start_date ? difference_in_months(end_date.to_date, art_start_date.to_date) < 3 : true
68
+ } }
69
+ end
70
+
71
+ def tpt_status_based_on_patient(patient)
72
+ tpt = determine_tpt(patient)
73
+ completed = patient_has_totally_completed_tpt?(patient, tpt)
74
+ tpt_init_date = patient['tpt_initiation_date']
75
+ tpt_current_expiry_date = patient['auto_expire_date']&.to_date
76
+ diff_in_months = difference_in_months(end_date.to_date, tpt_current_expiry_date)
77
+ art_start_date = ::Patient.find(patient_id).art_start_date
78
+ tpt_complete_date = completed ? patient['auto_expire_date']&.to_date : nil
79
+ tpt_end_date = calculate_tpt_end_date(tpt, tpt_init_date)
80
+ tpt_name = determine_tpt_name(tpt, patient)
81
+ arv_drug_runout_date = patient_arv_drug_runout_date
82
+
83
+ @tpt_status.merge!({ tpt: tpt_name, completed:, tb_treatment: false,
84
+ tpt_init_date:, tpt_complete_date:,
85
+ tpt_end_date:, art_start_date:,
86
+ art_drug_auto_expire_date: arv_drug_runout_date })
87
+ determine_eligibility(tpt, diff_in_months, art_start_date, arv_drug_runout_date)
88
+ end
89
+
90
+ def determine_tpt(patient)
91
+ patient_on_3hp?(patient) ? '3HP' : '6H'
92
+ end
93
+
94
+ def determine_tpt_name(tpt, patient)
95
+ if tpt == '6H'
96
+ 'IPT'
97
+ else
98
+ (patient['drug_concepts'].split(',').length > 1 ? '3HP (RFP + INH)' : 'INH 300 / RFP 300 (3HP)')
99
+ end
100
+ end
101
+
102
+ def calculate_tpt_end_date(tpt, tpt_init_date)
103
+ tpt == '6H' ? tpt_init_date + 6.months : tpt_init_date + 3.months
104
+ end
105
+
106
+ def determine_eligibility(tpt, diff_in_months, art_start_date, arv_drug_runout_date)
107
+ three_hp_eligible = false
108
+ six_h_eligible = false
109
+ tpt_init_date = @tpt_status[:tpt_init_date]
110
+ tpt_end_date = @tpt_status[:tpt_end_date]
111
+ case tpt
112
+ when '3HP'
113
+ # 3HP is taken 1 dose per week
114
+ # if client misses dose for less than a month, they are eligible
115
+ # if client misses more than a month:
116
+ # check if they have been on ART for less than 3 months, they are eligible
117
+ # if they have been on ART continuosly for more than 3 months, they are not eligible
118
+ three_hp_eligible = true if diff_in_months <= 1
119
+ if diff_in_months > 1 && (arv_drug_runout_date && difference_in_months(arv_drug_runout_date.to_date,
120
+ art_start_date.to_date) < 3)
121
+ # Patient defaulted for ART and TPT and was on ART for less than 3 months: patient TPT status is reset
122
+ three_hp_eligible = true
123
+ six_h_eligible = true
124
+ tpt_end_date = nil
125
+ tpt_init_date = nil
126
+ @tpt_status[:tpt] = nil
127
+ end
128
+ when '6H'
129
+ # 6H is taken 1 dose per day
130
+ # if client misses dose for less than 2 months, they are eligible
131
+ # if client misses more than a month:
132
+ # check if they have been on ART for less than 3 months, they are eligible
133
+ # if they have been on ART continuosly for more than 3 months, they are not eligible
134
+ six_h_eligible = true if diff_in_months <= 2
135
+ if diff_in_months > 2 && (arv_drug_runout_date && difference_in_months(arv_drug_runout_date.to_date,
136
+ art_start_date.to_date) < 3)
137
+ # Patient defaulted for ART and TPT and was on ART for less than 3 months: patient TPT status is reset
138
+ three_hp_eligible = true
139
+ six_h_eligible = true
140
+ tpt_end_date = nil
141
+ tpt_init_date = nil
142
+ @tpt_status[:tpt] = nil
143
+ end
144
+ end
145
+ @tpt_status.merge!({
146
+ tpt_init_date:,
147
+ tpt_end_date:,
148
+ eligible: {
149
+ '3HP': three_hp_eligible,
150
+ '6H': six_h_eligible
151
+ }
152
+ })
153
+ end
154
+
155
+ def patient_has_totally_completed_tpt?(patient, tpt)
156
+ if tpt == '3HP'
157
+ init_date = patient['tpt_initiation_date'].to_date
158
+ end_date = patient['auto_expire_date'].to_date
159
+ days_on_medication = (end_date - init_date).to_i
160
+ days_on_medication >= 80
161
+ else
162
+ patient['total_days_on_medication'].to_i >= 176
163
+ end
164
+ end
165
+
166
+ def patient_arv_drug_runout_date
167
+ ::Patient.find(patient_id).last_arv_drug_expire_date
168
+ end
169
+
170
+ def patient_history_on_completed_tpt
171
+ @patient_history_on_completed_tpt ||= ::Observation.where(person_id: patient_id,
172
+ concept_id: ::ConceptName.find_by_name('Previous TB treatment history').concept_id)
173
+ .where("value_text LIKE '%complete%' AND obs_datetime < DATE('#{end_date}') + INTERVAL 1 DAY")&.first&.value_text
174
+ end
175
+
176
+ def difference_in_months(date1, date2)
177
+ ((date1.year * 12) + date1.month) - ((date2.year * 12) + date2.month)
178
+ end
179
+ end
180
+ end
181
+ end