malawi_hiv_program_reports 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/app/services/malawi_hiv_program_reports/README.md +16 -0
  3. data/app/services/malawi_hiv_program_reports/adapters/moh/custom.rb +199 -0
  4. data/app/services/malawi_hiv_program_reports/archiving_candidates.rb +130 -0
  5. data/app/services/malawi_hiv_program_reports/arv_refill_periods.rb +311 -0
  6. data/app/services/malawi_hiv_program_reports/clinic/README.md +5 -0
  7. data/app/services/malawi_hiv_program_reports/clinic/appointments_report.rb +317 -0
  8. data/app/services/malawi_hiv_program_reports/clinic/discrepancy_report.rb +42 -0
  9. data/app/services/malawi_hiv_program_reports/clinic/docs/hypertension_report.md +31 -0
  10. data/app/services/malawi_hiv_program_reports/clinic/drug_dispensations.rb +48 -0
  11. data/app/services/malawi_hiv_program_reports/clinic/external_consultation_clients.rb +69 -0
  12. data/app/services/malawi_hiv_program_reports/clinic/hypertension_report.rb +223 -0
  13. data/app/services/malawi_hiv_program_reports/clinic/ipt_coverage.rb +112 -0
  14. data/app/services/malawi_hiv_program_reports/clinic/ipt_report.rb +69 -0
  15. data/app/services/malawi_hiv_program_reports/clinic/lims_results.rb +55 -0
  16. data/app/services/malawi_hiv_program_reports/clinic/outcome_list.rb +127 -0
  17. data/app/services/malawi_hiv_program_reports/clinic/patients_alive_and_on_treatment.rb +57 -0
  18. data/app/services/malawi_hiv_program_reports/clinic/patients_due_for_viral_load.rb +39 -0
  19. data/app/services/malawi_hiv_program_reports/clinic/patients_on_antiretrovirals.rb +44 -0
  20. data/app/services/malawi_hiv_program_reports/clinic/patients_on_dtg.rb +36 -0
  21. data/app/services/malawi_hiv_program_reports/clinic/patients_on_treatment.rb +42 -0
  22. data/app/services/malawi_hiv_program_reports/clinic/patients_with_outdated_demographics.rb +173 -0
  23. data/app/services/malawi_hiv_program_reports/clinic/pregnant_patients.rb +91 -0
  24. data/app/services/malawi_hiv_program_reports/clinic/regimen_dispensation_data.rb +282 -0
  25. data/app/services/malawi_hiv_program_reports/clinic/regimen_switch.rb +456 -0
  26. data/app/services/malawi_hiv_program_reports/clinic/regimens_and_formulations.rb +182 -0
  27. data/app/services/malawi_hiv_program_reports/clinic/regimens_by_weight_and_gender.rb +108 -0
  28. data/app/services/malawi_hiv_program_reports/clinic/retention.rb +246 -0
  29. data/app/services/malawi_hiv_program_reports/clinic/stock_card_report.rb +65 -0
  30. data/app/services/malawi_hiv_program_reports/clinic/tpt_outcome.rb +494 -0
  31. data/app/services/malawi_hiv_program_reports/clinic/tx_rtt.rb +169 -0
  32. data/app/services/malawi_hiv_program_reports/clinic/viral_load.rb +292 -0
  33. data/app/services/malawi_hiv_program_reports/clinic/viral_load_disaggregated.rb +97 -0
  34. data/app/services/malawi_hiv_program_reports/clinic/viral_load_results.rb +175 -0
  35. data/app/services/malawi_hiv_program_reports/clinic/visits_report.rb +113 -0
  36. data/app/services/malawi_hiv_program_reports/clinic/vl_collection.rb +48 -0
  37. data/app/services/malawi_hiv_program_reports/cohort/outcomes.rb +338 -0
  38. data/app/services/malawi_hiv_program_reports/cohort/regimens.rb +69 -0
  39. data/app/services/malawi_hiv_program_reports/cohort/side_effects.rb +141 -0
  40. data/app/services/malawi_hiv_program_reports/cohort/tpt.rb +172 -0
  41. data/app/services/malawi_hiv_program_reports/moh/cohort.rb +278 -0
  42. data/app/services/malawi_hiv_program_reports/moh/cohort_builder.rb +2340 -0
  43. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated.rb +608 -0
  44. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_additions.rb +208 -0
  45. data/app/services/malawi_hiv_program_reports/moh/cohort_disaggregated_builder.rb +526 -0
  46. data/app/services/malawi_hiv_program_reports/moh/cohort_struct.rb +219 -0
  47. data/app/services/malawi_hiv_program_reports/moh/cohort_survival_analysis.rb +203 -0
  48. data/app/services/malawi_hiv_program_reports/moh/moh_tpt.rb +223 -0
  49. data/app/services/malawi_hiv_program_reports/moh/tpt_newly_initiated.rb +235 -0
  50. data/app/services/malawi_hiv_program_reports/pepfar/defaulter_list.rb +25 -0
  51. data/app/services/malawi_hiv_program_reports/pepfar/maternal_status.rb +29 -0
  52. data/app/services/malawi_hiv_program_reports/pepfar/patient_start_vl.rb +45 -0
  53. data/app/services/malawi_hiv_program_reports/pepfar/regimen_switch.rb +479 -0
  54. data/app/services/malawi_hiv_program_reports/pepfar/sc_arvdisp.rb +174 -0
  55. data/app/services/malawi_hiv_program_reports/pepfar/sc_curr.rb +98 -0
  56. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev.rb +163 -0
  57. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev2.rb +222 -0
  58. data/app/services/malawi_hiv_program_reports/pepfar/tb_prev3.rb +421 -0
  59. data/app/services/malawi_hiv_program_reports/pepfar/tpt_status.rb +181 -0
  60. data/app/services/malawi_hiv_program_reports/pepfar/tx_ml.rb +181 -0
  61. data/app/services/malawi_hiv_program_reports/pepfar/tx_new.rb +202 -0
  62. data/app/services/malawi_hiv_program_reports/pepfar/tx_rtt.rb +288 -0
  63. data/app/services/malawi_hiv_program_reports/pepfar/tx_tb.rb +283 -0
  64. data/app/services/malawi_hiv_program_reports/pepfar/utils.rb +141 -0
  65. data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage.rb +414 -0
  66. data/app/services/malawi_hiv_program_reports/pepfar/viral_load_coverage2.rb +433 -0
  67. data/app/services/malawi_hiv_program_reports/report_map.rb +56 -0
  68. data/app/services/malawi_hiv_program_reports/utils/README.md +8 -0
  69. data/app/services/malawi_hiv_program_reports/utils/common_sql_query_utils.rb +60 -0
  70. data/app/services/malawi_hiv_program_reports/utils/concurrency_utils.rb +53 -0
  71. data/app/services/malawi_hiv_program_reports/utils/docs/common_sql_query_utils.md +53 -0
  72. data/app/services/malawi_hiv_program_reports/utils/model_utils.rb +66 -0
  73. data/app/services/malawi_hiv_program_reports/utils/parameter_utils.rb +32 -0
  74. data/app/services/malawi_hiv_program_reports/utils/time_utils.rb +52 -0
  75. data/lib/malawi_hiv_program_reports/version.rb +1 -1
  76. metadata +74 -1
@@ -0,0 +1,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,202 @@
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
+ MalawiHivProgramReports::Moh::CohortBuilder.new(location: @location).init_temporary_tables(start_date, end_date, '') if rebuild
26
+ process_data report
27
+ flatten_the_report report
28
+ rescue StandardError => e
29
+ Rails.logger.error e.message
30
+ Rails.logger.error e.backtrace.join("\n")
31
+
32
+ raise e
33
+ end
34
+
35
+ private
36
+
37
+ GENDER = %w[M F].freeze
38
+
39
+ def init_report
40
+ pepfar_age_groups.each_with_object({}) do |age_group, report|
41
+ report[age_group] = GENDER.each_with_object({}) do |gender, age_group_report|
42
+ age_group_report[gender] = {
43
+ cd4_less_than_200: [],
44
+ cd4_greater_than_equal_to_200: [],
45
+ cd4_unknown_or_not_done: [],
46
+ transfer_in: []
47
+ }
48
+ end
49
+ end
50
+ end
51
+
52
+ def addittional_groups(report)
53
+ report['All'] = {}
54
+ %w[Male FP FNP FBf].each do |key|
55
+ report['All'][key] = {
56
+ cd4_less_than_200: [],
57
+ cd4_greater_than_equal_to_200: [],
58
+ cd4_unknown_or_not_done: [],
59
+ transfer_in: []
60
+ }
61
+ end
62
+ end
63
+
64
+ def process_data(report)
65
+ data.each do |row|
66
+ age_group = row['age_group']
67
+ gender = row['gender']
68
+ date_enrolled = row['date_enrolled']
69
+ next if age_group.blank?
70
+ next if gender.blank?
71
+ next unless GENDER.include?(gender)
72
+ next unless pepfar_age_groups.include?(age_group)
73
+
74
+ cd4_count_group = row['cd4_count_group']
75
+ new_patient = row['new_patient'].to_i
76
+ patient_id = row['patient_id'].to_i
77
+ earliest_start_date = row['earliest_start_date']
78
+ indicator = new_patient.positive? ? cd4_count_group : 'transfer_in'
79
+
80
+ if new_patient.positive? && earliest_start_date.to_date >= start_date.to_date
81
+ report[age_group.to_s][gender.to_s][indicator.to_sym] << patient_id
82
+ elsif new_patient.zero?
83
+ report[age_group.to_s][gender.to_s][indicator.to_sym] << patient_id
84
+ else
85
+ next
86
+ end
87
+ process_aggreggation_rows(report: report, gender: gender, indicator: indicator, start_date: date_enrolled, patient_id: patient_id, maternal_status: row['maternal_status'], maternal_status_date: row['maternal_status_date'])
88
+ end
89
+ end
90
+
91
+ def process_aggreggation_rows(report:, gender:, indicator:, start_date:, **kwargs)
92
+ maternal_status = kwargs[:maternal_status]
93
+ maternal_status_date = kwargs[:maternal_status_date]
94
+
95
+ if gender == 'M'
96
+ report['All']['Male'][indicator.to_sym] << kwargs[:patient_id]
97
+ elsif maternal_status&.match?(/pregnant/i) && maternal_status_date&.to_date <= start_date.to_date
98
+ report['All']['FP'][indicator.to_sym] << kwargs[:patient_id]
99
+ elsif maternal_status&.match?(/breast/i) && maternal_status_date&.to_date <= start_date.to_date
100
+ report['All']['FBf'][indicator.to_sym] << kwargs[:patient_id]
101
+ else
102
+ report['All']['FNP'][indicator.to_sym] << kwargs[:patient_id]
103
+ end
104
+ end
105
+
106
+ def process_age_group_report(age_group, gender, age_group_report)
107
+ {
108
+ age_group: age_group,
109
+ gender: if gender == 'F'
110
+ 'Female'
111
+ else
112
+ (gender == 'M' ? 'Male' : gender)
113
+ end,
114
+ cd4_less_than_200: age_group_report['cd4_less_than_200'.to_sym],
115
+ cd4_greater_than_equal_to_200: age_group_report['cd4_greater_than_equal_to_200'.to_sym],
116
+ cd4_unknown_or_not_done: age_group_report['cd4_unknown_or_not_done'.to_sym],
117
+ transfer_in: age_group_report['transfer_in'.to_sym]
118
+ }
119
+ end
120
+
121
+ def flatten_the_report(report)
122
+ result = []
123
+ report.each do |age_group, age_group_report|
124
+ age_group_report.each_key do |gender|
125
+ result << process_age_group_report(age_group, gender, age_group_report[gender])
126
+ end
127
+ end
128
+
129
+ new_group = pepfar_age_groups.map { |age_group| age_group }
130
+ new_group << 'All'
131
+ gender_scores = { 'Female' => 0, 'Male' => 1, 'FNP' => 3, 'FP' => 2, 'FBf' => 4 }
132
+ result_scores = result.sort_by do |item|
133
+ gender_score = gender_scores[item[:gender]]
134
+ age_group_score = new_group.index(item[:age_group])
135
+ [gender_score, age_group_score]
136
+ end
137
+ # remove all unknown age groups
138
+ result_scores.reject { |item| item[:age_group].match?(/unknown/i) }
139
+ end
140
+
141
+
142
+ def data
143
+ ActiveRecord::Base.connection.select_all <<~SQL
144
+ SELECT
145
+ pp.patient_id,
146
+ MIN(pp.gender) gender,
147
+ disaggregated_age_group(pp.birthdate, DATE('#{end_date}')) age_group,
148
+ CASE
149
+ WHEN o.value_numeric < 200 THEN 'cd4_less_than_200'
150
+ WHEN o.value_numeric = 200 AND o.value_modifier = '=' THEN 'cd4_greater_than_equal_to_200'
151
+ WHEN o.value_numeric = 200 AND o.value_modifier = '<' THEN 'cd4_less_than_200'
152
+ WHEN o.value_numeric = 200 AND o.value_modifier = '>' THEN 'cd4_greater_than_equal_to_200'
153
+ WHEN o.value_numeric > 200 THEN 'cd4_greater_than_equal_to_200'
154
+ ELSE 'cd4_unknown_or_not_done'
155
+ END cd4_count_group,
156
+ CASE
157
+ WHEN transfer_in.value_coded IS NOT NULL THEN 0
158
+ ELSE 1
159
+ END new_patient,
160
+ pp.date_enrolled,
161
+ pp.earliest_start_date,
162
+ preg_or_breast.name AS maternal_status,
163
+ DATE(MIN(pregnant_or_breastfeeding.obs_datetime)) AS maternal_status_date
164
+ FROM temp_earliest_start_date pp
165
+ INNER JOIN person pe ON pe.person_id = pp.patient_id AND pe.voided = 0
166
+ #{site_manager(operator: 'AND', column: 'pe.site_id', location: @location)}
167
+ LEFT JOIN (
168
+ SELECT max(o.obs_datetime) AS obs_datetime, o.person_id
169
+ FROM obs o
170
+ INNER JOIN concept_name cn ON cn.concept_id = o.concept_id AND cn.name = 'CD4 count' AND cn.voided = 0
171
+ INNER JOIN patient_program pp ON pp.patient_id = o.person_id
172
+ AND pp.program_id = #{program('HIV PROGRAM').id}
173
+ AND pp.voided = 0
174
+ 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}')
175
+ WHERE o.concept_id = #{concept_name('CD4 count').concept_id} AND o.voided = 0
176
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
177
+ AND o.obs_datetime <= '#{end_date}' AND o.obs_datetime >= '#{start_date}'
178
+ GROUP BY o.person_id
179
+ ) current_cd4 ON current_cd4.person_id = pp.patient_id
180
+ 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
181
+ #{site_manager(operator: 'AND', column: 'o.site_id', location: @location)}
182
+ LEFT JOIN obs transfer_in ON transfer_in.person_id = pp.patient_id
183
+ AND transfer_in.concept_id = #{concept_name('Ever registered at ART clinic').concept_id}
184
+ AND transfer_in.voided = 0
185
+ AND transfer_in.value_coded = #{concept_name('Yes').concept_id}
186
+ AND transfer_in.obs_datetime <= '#{end_date}'
187
+ AND transfer_in.obs_datetime >= '#{start_date}'
188
+ #{site_manager(operator: 'AND', column: 'transfer_in.site_id', location: @location)}
189
+ LEFT JOIN obs pregnant_or_breastfeeding ON pregnant_or_breastfeeding.person_id = pp.patient_id
190
+ 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)
191
+ AND pregnant_or_breastfeeding.voided = 0
192
+ AND pregnant_or_breastfeeding.value_coded = #{concept_name('Yes').concept_id}
193
+ #{site_manager(operator: 'AND', column: 'pregnant_or_breastfeeding.site_id', location: @location)}
194
+ LEFT JOIN concept_name preg_or_breast ON preg_or_breast.concept_id = pregnant_or_breastfeeding.concept_id AND preg_or_breast.voided = 0
195
+ WHERE pp.date_enrolled <= '#{end_date}' AND pp.date_enrolled >= '#{start_date}'
196
+ GROUP BY pp.patient_id
197
+ SQL
198
+ end
199
+ end
200
+ # rubocop:enable Metrics/ClassLength
201
+ end
202
+ 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