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.
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 +2337 -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 +205 -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,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Pepfar
5
+ class TxMl
6
+ attr_reader :start_date, :end_date, :location
7
+
8
+ include Utils
9
+ include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
10
+ include MalawiHivProgramReports::Adapters::Moh::Custom
11
+
12
+ def initialize(start_date:, end_date:, **kwargs)
13
+ @start_date = start_date.to_date.strftime('%Y-%m-%d 00:00:00')
14
+ @end_date = end_date.to_date.strftime('%Y-%m-%d 23:59:59')
15
+ @occupation = kwargs[:occupation]
16
+ @location = kwargs[:location]
17
+ @adapter = ActiveRecord::Base.connection.adapter_name.downcase
18
+ end
19
+
20
+ def find_report
21
+ tx_ml
22
+ end
23
+
24
+ private
25
+
26
+ def tx_ml
27
+ data = {}
28
+ tx_curr = potential_tx_ml_clients
29
+ tx_new = new_potential_tx_ml_clients
30
+ patient_ids = []
31
+ earliest_start_dates = {}
32
+
33
+ (tx_curr || []).each do |pat|
34
+ patient_id = pat['patient_id'].to_i
35
+ patient_ids << patient_id
36
+ earliest_start_dates[patient_id] = begin
37
+ pat['earliest_start_date'].to_date
38
+ rescue StandardError
39
+ pat['date_enrolled'].to_date
40
+ end
41
+ end
42
+
43
+ (tx_new || []).each do |pat|
44
+ patient_id = pat['patient_id'].to_i
45
+ patient_ids << patient_id
46
+ patient_ids = patient_ids.uniq
47
+ earliest_start_dates[patient_id] = begin
48
+ pat['earliest_start_date'].to_date
49
+ rescue StandardError
50
+ pat['date_enrolled'].to_date
51
+ end
52
+ patient_ids << pat['patient_id']
53
+ patient_ids = patient_ids.uniq
54
+ end
55
+
56
+ return [] if patient_ids.blank?
57
+
58
+ filtered_patients = ActiveRecord::Base.connection.select_all <<~SQL
59
+ SELECT
60
+ p.person_id patient_id, birthdate, gender,
61
+ #{function_manager(function: 'pepfar_patient_outcome', location: @location, args: "p.person_id::int, '#{end_date.to_date}'::date, #{@location}::int")} outcome,
62
+ #{function_manager(function: 'disaggregated_age_group', location: @location, args: "p.birthdate::date, '#{end_date.to_date}::date'")} age_group
63
+ FROM person p
64
+ WHERE #{in_manager(column: 'p.person_id', values: patient_ids)}
65
+ #{site_manager(operator: 'AND', column: 'p.site_id', location: @location)}
66
+ GROUP BY p.person_id #{group_by_columns('p.birthdate, p.gender')}
67
+ SQL
68
+
69
+ (filtered_patients || []).each do |pat|
70
+ outcome = pat['outcome']
71
+ next if outcome == 'On antiretrovirals'
72
+
73
+ patient_id = pat['patient_id'].to_i
74
+ gender = begin
75
+ pat['gender'].first.upcase
76
+ rescue StandardError
77
+ 'Unknown'
78
+ end
79
+ age_group = pat['age_group']
80
+
81
+ if data[age_group].blank?
82
+ data[age_group] = {}
83
+ data[age_group][gender] = [[], [], [], [], [], []]
84
+ elsif data[age_group][gender].blank?
85
+ data[age_group][gender] = [[], [], [], [], [], []]
86
+ end
87
+
88
+ case outcome
89
+ when 'Defaulted'
90
+ def_months = defaulter_period(patient_id, earliest_start_dates[patient_id])
91
+ if def_months < 3
92
+ data[age_group][gender][1] << patient_id
93
+ elsif def_months <= 5
94
+ data[age_group][gender][2] << patient_id
95
+ elsif def_months > 5
96
+ data[age_group][gender][3] << patient_id
97
+ end
98
+ when 'Patient died'
99
+ data[age_group][gender][0] << patient_id
100
+ when /Stopped/i
101
+ data[age_group][gender][5] << patient_id
102
+ when 'Patient transferred out'
103
+ data[age_group][gender][4] << patient_id
104
+ end
105
+ rescue StandardError => e
106
+ Rails.logger.error(e.message)
107
+ end
108
+
109
+ data
110
+ end
111
+
112
+ def potential_tx_ml_clients
113
+ ActiveRecord::Base.connection.select_all <<~SQL
114
+ SELECT
115
+ p.patient_id AS patient_id,
116
+ pe.birthdate,
117
+ pe.gender,
118
+ #{function_manager(function: 'patient_date_enrolled', location: @location, args: "p.patient_id::int, #{@location}::int")} AS date_enrolled,
119
+ #{function_manager(function: 'date_antiretrovirals_started', location: @location, args: "p.patient_id::int, MIN(s.start_date)::date, #{@location}::int")} AS earliest_start_date
120
+ FROM patient_program p
121
+ INNER JOIN person pe ON pe.person_id = p.patient_id AND pe.voided = 0
122
+ #{site_manager(operator: 'AND', column: 'pe.site_id', location: @location)}
123
+ INNER JOIN patient_state s ON p.patient_program_id = s.patient_program_id AND s.voided = 0 AND s.state = 7
124
+ #{site_manager(operator: 'AND', column: 's.site_id', location: @location)}
125
+ LEFT JOIN (#{current_occupation_query}) a ON a.person_id = p.patient_id
126
+ WHERE p.program_id = 1 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
127
+ AND DATE(s.start_date) < '#{start_date.to_date}'
128
+ #{site_manager(operator: 'AND', column: 'p.site_id', location: @location)}
129
+ AND #{function_manager(function: 'pepfar_patient_outcome', location: @location, args: "p.patient_id::int, '#{start_date.to_date}'::date, #{@location}::int")} = 'On antiretrovirals'
130
+ AND pe.person_id NOT IN (#{drug_refills_and_external_consultation_list})
131
+ GROUP BY p.patient_id
132
+ HAVING date_enrolled IS NOT NULL AND DATE(date_enrolled) < '#{start_date.to_date}';
133
+ SQL
134
+ end
135
+
136
+ def new_potential_tx_ml_clients
137
+ ActiveRecord::Base.connection.select_all <<~SQL
138
+ SELECT
139
+ p.patient_id AS patient_id,
140
+ pe.birthdate,
141
+ pe.gender,
142
+ #{function_manager(function: 'patient_date_enrolled', location: @location, args: "p.patient_id::int, #{@location}::int")} AS date_enrolled,
143
+ #{function_manager(function: 'date_antiretrovirals_started', location: @location, args: "p.patient_id::int, MIN(s.start_date)::date, #{@location}::int")} AS earliest_start_date
144
+ FROM patient_program p
145
+ INNER JOIN person pe ON pe.person_id = p.patient_id AND pe.voided = 0
146
+ #{site_manager(operator: 'AND', column: 'pe.site_id', location: @location)}
147
+ INNER JOIN patient_state s ON p.patient_program_id = s.patient_program_id AND s.voided = 0 AND s.state = 7
148
+ #{site_manager(operator: 'AND', column: 's.site_id', location: @location)}
149
+ LEFT JOIN (#{current_occupation_query}) a ON a.person_id = p.patient_id
150
+ WHERE p.program_id = 1 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
151
+ AND DATE(s.start_date) BETWEEN DATE('#{start_date}') AND DATE('#{end_date}')
152
+ AND pe.person_id NOT IN (#{drug_refills_and_external_consultation_list})
153
+ #{site_manager(operator: 'AND', column: 'p.site_id', location: @location)}
154
+ GROUP BY p.patient_id
155
+ HAVING date_enrolled IS NOT NULL AND date_enrolled BETWEEN DATE('#{start_date}') AND DATE('#{end_date}');
156
+ SQL
157
+ end
158
+
159
+ def defaulter_period(patient_id, earliest_start_date)
160
+ defaulter_date = ActiveRecord::Base.connection.select_one <<~SQL
161
+ SELECT #{function_manager(function: 'current_pepfar_defaulter_date', location: @location, args: "#{patient_id}::int, '#{end_date}'::date, #{@location}::int")}
162
+ SQL
163
+
164
+ defaulter_date = begin
165
+ defaulter_date['def_date'].to_date
166
+ rescue StandardError
167
+ end_date.to_date
168
+ end
169
+
170
+ raise 'Defaulted outside the reporting period' if defaulter_date > end_date.to_date
171
+ raise 'Defaulted outside the reporting period' if defaulter_date < start_date.to_date
172
+
173
+ days_gone = ActiveRecord::Base.connection.select_one <<~SQL
174
+ SELECT TIMESTAMPDIFF(MONTH, DATE('#{earliest_start_date}'), DATE('#{defaulter_date}')) months;
175
+ SQL
176
+
177
+ days_gone['months'].to_i
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Pepfar
5
+ # This class is responsible for generating the tx_new report
6
+ # rubocop:disable Metrics/ClassLength
7
+ class TxNew
8
+ include MalawiHivProgramReports::Utils::ModelUtils
9
+ include Pepfar::Utils
10
+ include MalawiHivProgramReports::Adapters::Moh::Custom
11
+
12
+ attr_reader :start_date, :end_date, :rebuild, :location
13
+
14
+ def initialize(start_date:, end_date:, **kwargs)
15
+ @start_date = start_date.to_date.beginning_of_day.strftime('%Y-%m-%d %H:%M:%S')
16
+ @end_date = end_date.to_date.end_of_day.strftime('%Y-%m-%d %H:%M:%S')
17
+ rebuild_string = kwargs[:rebuild]
18
+ @rebuild = rebuild_string.present? ? rebuild_string&.downcase == 'true' : false
19
+ @location = kwargs[:location]
20
+ end
21
+
22
+ def find_report
23
+ report = init_report
24
+ addittional_groups report
25
+ if rebuild
26
+ MalawiHivProgramReports::Moh::CohortBuilder.new(location: @location).init_temporary_tables(start_date,
27
+ end_date, '')
28
+ end
29
+ process_data report
30
+ flatten_the_report report
31
+ rescue StandardError => e
32
+ Rails.logger.error e.message
33
+ Rails.logger.error e.backtrace.join("\n")
34
+
35
+ raise e
36
+ end
37
+
38
+ private
39
+
40
+ GENDER = %w[M F].freeze
41
+
42
+ def init_report
43
+ pepfar_age_groups.each_with_object({}) do |age_group, report|
44
+ report[age_group] = GENDER.each_with_object({}) do |gender, age_group_report|
45
+ age_group_report[gender] = {
46
+ cd4_less_than_200: [],
47
+ cd4_greater_than_equal_to_200: [],
48
+ cd4_unknown_or_not_done: [],
49
+ transfer_in: []
50
+ }
51
+ end
52
+ end
53
+ end
54
+
55
+ def addittional_groups(report)
56
+ report['All'] = {}
57
+ %w[Male FP FNP FBf].each do |key|
58
+ report['All'][key] = {
59
+ cd4_less_than_200: [],
60
+ cd4_greater_than_equal_to_200: [],
61
+ cd4_unknown_or_not_done: [],
62
+ transfer_in: []
63
+ }
64
+ end
65
+ end
66
+
67
+ def process_data(report)
68
+ data.each do |row|
69
+ age_group = row['age_group']
70
+ gender = row['gender']
71
+ date_enrolled = row['date_enrolled']
72
+ next if age_group.blank?
73
+ next if gender.blank?
74
+ next unless GENDER.include?(gender)
75
+ next unless pepfar_age_groups.include?(age_group)
76
+
77
+ cd4_count_group = row['cd4_count_group']
78
+ new_patient = row['new_patient'].to_i
79
+ patient_id = row['patient_id'].to_i
80
+ earliest_start_date = row['earliest_start_date']
81
+ indicator = new_patient.positive? ? cd4_count_group : 'transfer_in'
82
+
83
+ if new_patient.positive? && earliest_start_date.to_date >= start_date.to_date
84
+ report[age_group.to_s][gender.to_s][indicator.to_sym] << patient_id
85
+ elsif new_patient.zero?
86
+ report[age_group.to_s][gender.to_s][indicator.to_sym] << patient_id
87
+ else
88
+ next
89
+ end
90
+ process_aggreggation_rows(report:, gender:, indicator:, start_date: date_enrolled,
91
+ patient_id:, maternal_status: row['maternal_status'], maternal_status_date: row['maternal_status_date'])
92
+ end
93
+ end
94
+
95
+ def process_aggreggation_rows(report:, gender:, indicator:, start_date:, **kwargs)
96
+ maternal_status = kwargs[:maternal_status]
97
+ maternal_status_date = kwargs[:maternal_status_date]
98
+
99
+ if gender == 'M'
100
+ report['All']['Male'][indicator.to_sym] << kwargs[:patient_id]
101
+ elsif maternal_status&.match?(/pregnant/i) && (maternal_status_date&.to_date&.<= start_date.to_date)
102
+ report['All']['FP'][indicator.to_sym] << kwargs[:patient_id]
103
+ elsif maternal_status&.match?(/breast/i) && (maternal_status_date&.to_date&.<= start_date.to_date)
104
+ report['All']['FBf'][indicator.to_sym] << kwargs[:patient_id]
105
+ else
106
+ report['All']['FNP'][indicator.to_sym] << kwargs[:patient_id]
107
+ end
108
+ end
109
+
110
+ def process_age_group_report(age_group, gender, age_group_report)
111
+ {
112
+ age_group:,
113
+ gender: if gender == 'F'
114
+ 'Female'
115
+ else
116
+ (gender == 'M' ? 'Male' : gender)
117
+ end,
118
+ cd4_less_than_200: age_group_report[:cd4_less_than_200],
119
+ cd4_greater_than_equal_to_200: age_group_report[:cd4_greater_than_equal_to_200],
120
+ cd4_unknown_or_not_done: age_group_report[:cd4_unknown_or_not_done],
121
+ transfer_in: age_group_report[:transfer_in]
122
+ }
123
+ end
124
+
125
+ def flatten_the_report(report)
126
+ result = []
127
+ report.each do |age_group, age_group_report|
128
+ age_group_report.each_key do |gender|
129
+ result << process_age_group_report(age_group, gender, age_group_report[gender])
130
+ end
131
+ end
132
+
133
+ new_group = pepfar_age_groups.map { |age_group| age_group }
134
+ new_group << 'All'
135
+ gender_scores = { 'Female' => 0, 'Male' => 1, 'FNP' => 3, 'FP' => 2, 'FBf' => 4 }
136
+ result_scores = result.sort_by do |item|
137
+ gender_score = gender_scores[item[:gender]]
138
+ age_group_score = new_group.index(item[:age_group])
139
+ [gender_score, age_group_score]
140
+ end
141
+ # remove all unknown age groups
142
+ result_scores.reject { |item| item[:age_group].match?(/unknown/i) }
143
+ end
144
+
145
+ def data
146
+ ActiveRecord::Base.connection.select_all <<~SQL
147
+ SELECT
148
+ pp.patient_id,
149
+ MIN(pp.gender) gender,
150
+ disaggregated_age_group(pp.birthdate, DATE('#{end_date}')) age_group,
151
+ CASE
152
+ WHEN o.value_numeric < 200 THEN 'cd4_less_than_200'
153
+ WHEN o.value_numeric = 200 AND o.value_modifier = '=' THEN 'cd4_greater_than_equal_to_200'
154
+ WHEN o.value_numeric = 200 AND o.value_modifier = '<' THEN 'cd4_less_than_200'
155
+ WHEN o.value_numeric = 200 AND o.value_modifier = '>' THEN 'cd4_greater_than_equal_to_200'
156
+ WHEN o.value_numeric > 200 THEN 'cd4_greater_than_equal_to_200'
157
+ ELSE 'cd4_unknown_or_not_done'
158
+ END cd4_count_group,
159
+ CASE
160
+ WHEN transfer_in.value_coded IS NOT NULL THEN 0
161
+ ELSE 1
162
+ END new_patient,
163
+ pp.date_enrolled,
164
+ pp.earliest_start_date,
165
+ preg_or_breast.name AS maternal_status,
166
+ DATE(MIN(pregnant_or_breastfeeding.obs_datetime)) AS maternal_status_date
167
+ FROM temp_earliest_start_date pp
168
+ INNER JOIN person pe ON pe.person_id = pp.patient_id AND pe.voided = 0
169
+ #{site_manager(operator: 'AND', column: 'pe.site_id', location: @location)}
170
+ LEFT JOIN (
171
+ SELECT max(o.obs_datetime) AS obs_datetime, o.person_id
172
+ FROM obs o
173
+ INNER JOIN concept_name cn ON cn.concept_id = o.concept_id AND cn.name = 'CD4 count' AND cn.voided = 0
174
+ INNER JOIN patient_program pp ON pp.patient_id = o.person_id
175
+ AND pp.program_id = #{program('HIV PROGRAM').id}
176
+ AND pp.voided = 0
177
+ INNER JOIN patient_state ps ON ps.patient_program_id = pp.patient_program_id AND ps.voided = 0 AND ps.state = 7 AND ps.start_date <= DATE('#{end_date}')
178
+ WHERE o.concept_id = #{concept_name('CD4 count').concept_id} AND o.voided = 0
179
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}#{' '}
180
+ AND o.obs_datetime <= '#{end_date}' AND o.obs_datetime >= '#{start_date}'
181
+ GROUP BY o.person_id
182
+ ) current_cd4 ON current_cd4.person_id = pp.patient_id
183
+ LEFT JOIN obs o ON o.person_id = pp.patient_id AND o.concept_id = #{concept_name('CD4 count').concept_id} AND o.voided = 0 AND o.obs_datetime = current_cd4.obs_datetime
184
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
185
+ LEFT JOIN obs transfer_in ON transfer_in.person_id = pp.patient_id
186
+ AND transfer_in.concept_id = #{concept_name('Ever registered at ART clinic').concept_id}
187
+ AND transfer_in.voided = 0
188
+ AND transfer_in.value_coded = #{concept_name('Yes').concept_id}
189
+ AND transfer_in.obs_datetime <= '#{end_date}'
190
+ AND transfer_in.obs_datetime >= '#{start_date}'
191
+ #{site_manager(operator: 'AND', column: 'transfer_in.site_id', location: @location)}
192
+ LEFT JOIN obs pregnant_or_breastfeeding ON pregnant_or_breastfeeding.person_id = pp.patient_id
193
+ AND pregnant_or_breastfeeding.concept_id IN (SELECT concept_id FROM concept_name WHERE name IN ('Breast feeding?', 'Breast feeding', 'Breastfeeding', 'Is patient pregnant?', 'patient pregnant') AND voided = 0)
194
+ AND pregnant_or_breastfeeding.voided = 0
195
+ AND pregnant_or_breastfeeding.value_coded = #{concept_name('Yes').concept_id}
196
+ #{site_manager(operator: 'AND', column: 'pregnant_or_breastfeeding.site_id', location: @location)}
197
+ LEFT JOIN concept_name preg_or_breast ON preg_or_breast.concept_id = pregnant_or_breastfeeding.concept_id AND preg_or_breast.voided = 0
198
+ WHERE pp.date_enrolled <= '#{end_date}' AND pp.date_enrolled >= '#{start_date}'
199
+ GROUP BY pp.patient_id
200
+ SQL
201
+ end
202
+ end
203
+ # rubocop:enable Metrics/ClassLength
204
+ end
205
+ end
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+ module MalawiHivProgramReports
3
+ module Pepfar
4
+ class TxRtt
5
+
6
+ include Utils
7
+ include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
8
+ include MalawiHivProgramReports::Adapters::Moh::Custom
9
+ include MalawiHivProgramReports::Utils::ModelUtils
10
+ attr_reader :start_date, :end_date, :location
11
+
12
+ def initialize(start_date:, end_date:, **kwargs)
13
+ @start_date = ActiveRecord::Base.connection.quote(start_date.to_date.beginning_of_day.strftime('%Y-%m-%d %H:%M:%S'))
14
+ @end_date = ActiveRecord::Base.connection.quote(end_date.to_date.end_of_day.strftime('%Y-%m-%d %H:%M:%S'))
15
+ @occupation = kwargs[:occupation]
16
+ @location = kwargs[:location]
17
+ end
18
+
19
+ def find_report
20
+ process_report
21
+ end
22
+
23
+ def data
24
+ process_report
25
+ rescue StandardError => e
26
+ Rails.logger.error "Error running TX_RTT Report: #{e}"
27
+ Rails.logger.error e.backtrace.join("\n")
28
+ raise e
29
+ end
30
+
31
+ private
32
+
33
+ GENDER = %w[M F].freeze
34
+
35
+ def init_report
36
+ pepfar_age_groups.each_with_object({}) do |age_group, age_group_report|
37
+ age_group_report[age_group] = GENDER.each_with_object({}) do |gender, gender_report|
38
+ gender_report[gender] = indicators
39
+ end
40
+ end
41
+ end
42
+
43
+ def process_report
44
+ report = init_report
45
+ process_data report
46
+ flatten_the_report report
47
+ end
48
+
49
+ def process_data(report)
50
+ tx_rtt.each do |row|
51
+ age_group = row['age_group']
52
+ gender = row['gender']
53
+ months = row['months']
54
+ patient_id = row['patient_id']
55
+ cd4_cat = row['cd4_count_group']
56
+
57
+ next unless GENDER.include?(gender)
58
+ next unless pepfar_age_groups.include?(age_group)
59
+
60
+ cat = report[age_group][gender]
61
+ process_months(cat, months, patient_id)
62
+ process_cd4(cat, months, patient_id, cd4_cat)
63
+ end
64
+ end
65
+
66
+ def process_months(report, months, patient_id)
67
+ report[:returned_less_than_3_months] << patient_id if months.blank?
68
+ report[:returned_less_than_3_months] << patient_id if months < 3
69
+ report[:returned_greater_than_3_months_and_less_than_6_months] << patient_id if months >= 3 && months < 6
70
+ report[:returned_greater_than_or_equal_to_6_months] << patient_id if months >= 6
71
+ end
72
+
73
+ def process_cd4(report, months, patient_id, cd4_cat)
74
+ if cd4_cat == 'unknown_cd4_count' && months <= 2
75
+ report[:not_eligible_for_cd4] << patient_id
76
+ return
77
+ end
78
+
79
+ report[cd4_cat.to_sym] << patient_id
80
+ end
81
+
82
+ def indicators
83
+ {
84
+ 'cd4_less_than_200': [],
85
+ 'cd4_greater_than_or_equal_to_200': [],
86
+ 'unknown_cd4_count': [],
87
+ 'not_eligible_for_cd4': [],
88
+ 'returned_less_than_3_months': [],
89
+ 'returned_greater_than_3_months_and_less_than_6_months': [],
90
+ 'returned_greater_than_or_equal_to_6_months': []
91
+ }
92
+ end
93
+
94
+ def process_age_group_report(age_group, gender, age_group_report)
95
+ {
96
+ age_group: age_group,
97
+ gender: gender,
98
+ cd4_less_than_200: age_group_report[:cd4_less_than_200],
99
+ cd4_greater_than_or_equal_to_200: age_group_report[:cd4_greater_than_or_equal_to_200],
100
+ unknown_cd4_count: age_group_report[:unknown_cd4_count],
101
+ not_eligible_for_cd4: age_group_report[:not_eligible_for_cd4],
102
+ returned_less_than_3_months: age_group_report[:returned_less_than_3_months],
103
+ returned_greater_than_3_months_and_less_than_6_months: age_group_report[:returned_greater_than_3_months_and_less_than_6_months],
104
+ returned_greater_than_or_equal_to_6_months: age_group_report[:returned_greater_than_or_equal_to_6_months]
105
+ }
106
+ end
107
+
108
+ def flatten_the_report(report)
109
+ result = []
110
+ report.each do |age_group, age_group_report|
111
+ result << process_age_group_report(age_group, 'M', age_group_report['M'])
112
+ result << process_age_group_report(age_group, 'F', age_group_report['F'])
113
+ end
114
+ sorted_results = result.sort_by do |item|
115
+ gender_score = item[:gender] == 'Female' ? 0 : 1
116
+ age_group_score = pepfar_age_groups.index(item[:age_group])
117
+ [gender_score, age_group_score]
118
+ end
119
+ # sort by gender, start all females and push all males to the end
120
+ sorted_results.sort_by { |h| [h[:gender] == 'F' ? 0 : 1] }
121
+ end
122
+
123
+ def tx_rtt
124
+ ActiveRecord::Base.connection.select_all <<~SQL
125
+ SELECT patient_program.patient_id,
126
+ #{function_manager(function: 'disaggregated_age_group', location: @location, args: "person.birthdate::date, #{@end_date}::date")} AS age_group,
127
+ person.gender,
128
+ IF(
129
+ patient_state_at_start_of_quarter.state = 6, 'Treatment stopped',
130
+ IF(
131
+ patient_state_at_start_of_quarter.state = 12, 'Defaulted',
132
+ #{function_manager(function: 'pepfar_patient_outcome', location: @location, args: "patient_program.patient_id, (DATE(#{@start_date}) - INTERVAL 1 DAY), #{@location}")}
133
+ )
134
+ ) AS initial_outcome,
135
+ IF(
136
+ patient_state_at_start_of_quarter.state = 6,
137
+ patient_state_at_start_of_quarter.start_date,
138
+ IF(
139
+ patient_state_at_start_of_quarter.state = 12,
140
+ patient_state_at_start_of_quarter.start_date,
141
+ #{function_manager(function: 'current_pepfar_defaulter_date', location: @location, args: "patient_program.patient_id, (DATE(#{@start_date}) - INTERVAL 1 DAY), #{@location}")}
142
+ )) AS initial_outcome_date,
143
+ IF(
144
+ patients_with_orders_at_end_of_quarter.patient_id IS NOT NULL, 'On antiretrovirals',
145
+ IF(
146
+ #{function_manager(function: 'current_pepfar_defaulter', location: @location, args: "patient_program.patient_id, #{@end_date}, #{@location}")} = 0,
147
+ 'On antiretrovirals','Defaulted'
148
+ )
149
+ ) AS final_outcome,
150
+ TIMESTAMPDIFF(MONTH, IF(
151
+ patient_state_at_start_of_quarter.state = 6,
152
+ patient_state_at_start_of_quarter.start_date,
153
+ IF(
154
+ patient_state_at_start_of_quarter.state = 12,
155
+ patient_state_at_start_of_quarter.start_date,
156
+ #{function_manager(function: 'current_pepfar_defaulter_date', location: @location, args: "patient_program.patient_id, (DATE(#{@start_date}) - INTERVAL 1 DAY), #{@location}")})
157
+ ), patients_who_received_art_in_quarter.start_date) AS months,
158
+ CASE
159
+ WHEN cd4_result.value_numeric < 200 THEN 'cd4_less_than_200'
160
+ WHEN cd4_result.value_numeric = 200 AND cd4_result.value_modifier = '=' THEN 'cd4_greater_than_or_equal_to_200'
161
+ WHEN cd4_result.value_numeric = 200 AND cd4_result.value_modifier = '<' THEN 'cd4_less_than_200'
162
+ WHEN cd4_result.value_numeric = 200 AND cd4_result.value_modifier = '>' THEN 'cd4_greater_than_or_equal_to_200'
163
+ WHEN cd4_result.value_numeric > 200 THEN 'cd4_greater_than_or_equal_to_200'
164
+ ELSE 'unknown_cd4_count'
165
+ END cd4_count_group
166
+ FROM patient_program
167
+ INNER JOIN person ON person.person_id = patient_program.patient_id
168
+ #{site_manager(operator: 'AND', column: 'person.site_id', location: @location)}
169
+ /* Select patients that were on treatment before start of reporting period */
170
+ INNER JOIN patient_state AS patient_ever_on_treatment
171
+ ON patient_ever_on_treatment.patient_program_id = patient_program.patient_program_id
172
+ AND patient_ever_on_treatment.state = 7
173
+ AND patient_ever_on_treatment.start_date < DATE(#{start_date})
174
+ AND patient_ever_on_treatment.voided = 0
175
+ /* Get patient's state at the start of the quarter. */
176
+ INNER JOIN (
177
+ SELECT patient_program_id, MAX(patient_state.date_created) AS date_created
178
+ FROM patient_state
179
+ INNER JOIN patient_program USING (patient_program_id)
180
+ WHERE patient_state.voided = 0
181
+ AND patient_program.voided = 0
182
+ AND patient_program.program_id = 1
183
+ AND patient_state.start_date < DATE(#{start_date}) + INTERVAL 1 DAY
184
+ GROUP BY patient_program_id
185
+ ) AS date_of_last_patient_state_before_quarter
186
+ ON date_of_last_patient_state_before_quarter.patient_program_id = patient_program.patient_program_id
187
+ LEFT JOIN patient_state AS patient_state_at_start_of_quarter
188
+ ON patient_state_at_start_of_quarter.patient_program_id = date_of_last_patient_state_before_quarter.patient_program_id
189
+ #{site_manager(operator: 'AND', column: 'patient_state_at_start_of_quarter.site_id', location: @location)}
190
+ AND patient_state_at_start_of_quarter.date_created = date_of_last_patient_state_before_quarter.date_created
191
+ AND patient_state_at_start_of_quarter.state IN (6, 12) /* 2: TO, 6: Tx Stopped, 12: Defaulted */
192
+ /* Select patients who received ART within the reporting period. */
193
+ INNER JOIN (
194
+ SELECT DISTINCT encounter.patient_id, orders.start_date
195
+ FROM encounter
196
+ INNER JOIN orders
197
+ ON orders.encounter_id = encounter.encounter_id
198
+ AND DATE(orders.start_date) BETWEEN DATE(#{start_date}) AND DATE(#{end_date})
199
+ AND orders.voided = 0
200
+ INNER JOIN drug_order
201
+ ON drug_order.order_id = orders.order_id
202
+ AND drug_order.quantity > 0
203
+ AND drug_order.drug_inventory_id IN (SELECT DISTINCT drug_id FROM arv_drug)
204
+ WHERE encounter.voided = 0
205
+ AND encounter.program_id = 1
206
+ AND DATE(encounter.encounter_datetime) BETWEEN DATE(#{start_date}) AND DATE(#{end_date})
207
+ ) AS patients_who_received_art_in_quarter
208
+ ON patients_who_received_art_in_quarter.patient_id = patient_program.patient_id
209
+ /* Ensure that patients are on ART at the end of the quarter */
210
+ INNER JOIN (
211
+ SELECT patient_program_id, MAX(patient_state.date_created) AS date_created
212
+ FROM patient_state
213
+ INNER JOIN patient_program USING (patient_program_id)
214
+ WHERE patient_state.voided = 0
215
+ AND patient_program.voided = 0
216
+ AND patient_program.program_id = 1
217
+ AND patient_state.start_date < DATE(#{end_date}) + INTERVAL 1 DAY
218
+ GROUP BY patient_program_id
219
+ ) AS date_of_last_patient_state_in_quarter
220
+ ON date_of_last_patient_state_in_quarter.patient_program_id = patient_program.patient_program_id
221
+
222
+ /*Not sure why Walter had this section in but I believe its not neccessary*/
223
+ /*INNER JOIN patient_state AS patient_state_at_end_of_quarter
224
+ ON patient_state_at_end_of_quarter.patient_program_id = patient_program.patient_program_id
225
+ AND patient_state_at_end_of_quarter.date_created = date_of_last_patient_state_before_quarter.date_created
226
+ AND patient_state_at_end_of_quarter.state = 7*/
227
+
228
+ /* Select patient who had orders in the last 30 days of the reporting period.
229
+ This is to be used as a quick filter for patients who are definitely
230
+ 'On ART' by the end of the reporting period. The rest will be filtered by
231
+ the current_defaulter function. */
232
+ LEFT JOIN (
233
+ SELECT DISTINCT encounter.patient_id
234
+ FROM encounter
235
+ INNER JOIN orders
236
+ ON orders.encounter_id = encounter.encounter_id
237
+ AND orders.voided = 0
238
+ #{site_manager(operator: 'AND', column: 'orders.site_id', location: @location)}
239
+ AND DATE(orders.start_date) BETWEEN DATE(#{start_date}) AND DATE(#{end_date})
240
+ AND DATE(orders.auto_expire_date) >= (DATE(#{end_date}) - INTERVAL 30 DAY)
241
+ INNER JOIN drug_order
242
+ ON drug_order.order_id = orders.order_id
243
+ AND drug_order.quantity > 0
244
+ AND drug_order.drug_inventory_id IN (SELECT DISTINCT drug_id FROM arv_drug)
245
+ WHERE encounter.program_id = 1
246
+ AND DATE(encounter.encounter_datetime) BETWEEN DATE(#{start_date}) AND DATE(#{end_date})
247
+ AND encounter.voided = 0
248
+ ) AS patients_with_orders_at_end_of_quarter
249
+ ON patients_with_orders_at_end_of_quarter.patient_id = patient_program.patient_id
250
+ LEFT JOIN (
251
+ SELECT max(o.obs_datetime) AS obs_datetime, o.person_id
252
+ FROM obs o
253
+ INNER JOIN concept_name cn ON cn.concept_id = o.concept_id AND cn.name = 'CD4 count'
254
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
255
+ WHERE o.concept_id = #{concept('CD4 count').concept_id} AND o.voided = 0
256
+ AND o.obs_datetime <= #{end_date} AND o.obs_datetime >= #{start_date}
257
+ GROUP BY o.person_id
258
+ ) current_cd4 ON current_cd4.person_id = patient_program.patient_id
259
+ LEFT JOIN obs cd4_result ON cd4_result.person_id = patient_program.patient_id AND cd4_result.concept_id = #{concept('CD4 count').concept_id} AND cd4_result.voided = 0
260
+ #{site_manager(operator: 'AND', column: 'cd4_result.site_id', location: @location)}
261
+ # Not sure why we are matching the two dates but the result was excluding cd4 results for patients.
262
+ # AND cd4_result.obs_datetime = current_cd4.obs_datetime
263
+ LEFT JOIN (#{current_occupation_query}) a ON a.person_id = patient_program.patient_id
264
+ WHERE patient_program.program_id = 1 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
265
+ /* Ensure that the patients retrieved, did not receive ART within 28 days
266
+ before the start of the reporting period */
267
+ AND patient_program.patient_id NOT IN (
268
+ SELECT DISTINCT orders.patient_id
269
+ FROM orders
270
+ INNER JOIN drug_order USING (order_id)
271
+ INNER JOIN arv_drug ON arv_drug.drug_id = drug_inventory_id
272
+ INNER JOIN patient_program
273
+ ON patient_program.patient_id = orders.patient_id
274
+ AND patient_program.program_id = 1
275
+ #{site_manager(operator: 'AND', column: 'orders.site_id', location: @location)}
276
+ WHERE ((DATE(orders.start_date )BETWEEN (DATE(#{start_date}) - INTERVAL 30 DAY) AND DATE(#{start_date}))
277
+ OR (DATE(orders.auto_expire_date) BETWEEN (DATE(#{start_date}) - INTERVAL 30 DAY) AND DATE(#{start_date})))
278
+ AND orders.voided = 0
279
+ )
280
+ GROUP BY patient_program.patient_id
281
+ HAVING initial_outcome IN ('Defaulted', 'Treatment stopped')
282
+ AND final_outcome = 'On antiretrovirals'
283
+ AND initial_outcome_date IS NOT NULL;
284
+ SQL
285
+ end
286
+ end
287
+ end
288
+ end