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,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Retrieve all patients who are taking ARVs in a given time period.
4
+ module MalawiHivProgramReports
5
+ module Clinic
6
+ class PatientsOnAntiretrovirals
7
+ attr_reader :start_date, :end_date
8
+
9
+ def initialize(start_date:, end_date:, **_kwargs)
10
+ @start_date = start_date
11
+ @end_date = end_date
12
+ end
13
+
14
+ def self.within(start_date, end_date)
15
+ PatientsOnAntiretrovirals.new(start_date:, end_date:)
16
+ .patients
17
+ end
18
+
19
+ def find_report
20
+ patients
21
+ end
22
+
23
+ def patients
24
+ ::DrugOrder.select('orders.patient_id AS patient_id')
25
+ .joins(:order)
26
+ .merge(art_orders)
27
+ .where(drug_inventory_id: ArtService::RegimenEngine.arv_drugs,
28
+ quantity: 1..Float::INFINITY)
29
+ .where('start_date BETWEEN :start_date AND :end_date
30
+ OR auto_expire_date BETWEEN :start_date AND :end_date
31
+ OR (start_date <= :start_date AND auto_expire_date >= :end_date)',
32
+ start_date:, end_date:)
33
+ .group('orders.patient_id')
34
+ end
35
+
36
+ private
37
+
38
+ def art_orders
39
+ ::Order.joins(:encounter)
40
+ .where('encounter.program_id = ?', ::ArtService::Constants::PROGRAM_ID)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MalawiHivProgramReports
4
+ module Clinic
5
+ class PatientsOnDtg
6
+ attr_reader :start_date, :end_date
7
+
8
+ HIV_PROGRAM_ID = 1
9
+ ARV_NUMBER_TYPE_ID = 4
10
+
11
+ def initialize(start_date:, end_date:, **_)
12
+ @start_date = start_date
13
+ @end_date = end_date
14
+ end
15
+
16
+ def find_report
17
+ ::DrugOrder.joins(:order)
18
+ .joins('INNER JOIN encounter USING (encounter_id)')
19
+ .joins('LEFT JOIN patient_identifier ON patient_identifier.patient_id = orders.patient_id')
20
+ .where(drug: dtg_drugs,
21
+ encounter: { program_id: HIV_PROGRAM_ID },
22
+ patient_identifier: { identifier_type: ARV_NUMBER_TYPE_ID })
23
+ .where('start_date BETWEEN ? AND ?', start_date, end_date)
24
+ .group('orders.patient_id')
25
+ .select('identifier')
26
+ .map(&:identifier)
27
+ end
28
+
29
+ private
30
+
31
+ def dtg_drugs
32
+ ::Drug.where(concept_id: ::ConceptName.find_by_name('Dolutegravir').concept_id)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Reports on all patients that were on treatment in given time period
4
+ # regardless of whether they have a terminal state or not within the
5
+ # period.
6
+ module MalawiHivProgramReports
7
+ module Clinic
8
+ class PatientsOnTreatment
9
+ attr_reader :start_date, :end_date
10
+
11
+ def initialize(start_date:, end_date:)
12
+ @start_date = start_date
13
+ @end_date = end_date
14
+ end
15
+
16
+ # Returns patients that were on treatment within the given time period.
17
+ def self.within(start_date, end_date)
18
+ sql_conditions =
19
+ <<~SQL
20
+ ((start_date >= :start_date AND start_date <= :end_date)
21
+ OR (end_date >= :start_date AND end_date <= :end_date)
22
+ OR (start_date < :start_date AND end_date IS NULL))
23
+ AND state = :state
24
+ SQL
25
+
26
+ on_arvs = ::PatientState.where(sql_conditions, start_date:,
27
+ end_date:,
28
+ state: ::ArtService::Constants::States::ON_ANTIRETROVIRALS)
29
+
30
+ ::PatientProgram.select('DISTINCT patient_program.patient_id')
31
+ .joins(:patient_states)
32
+ .merge(on_arvs)
33
+ .where(program_id: ::ArtService::Constants::PROGRAM_ID)
34
+ end
35
+
36
+ # We an interface to satisfy, let's be good citizens
37
+ def find_report
38
+ within(start_date, end_date).map(&:patient_id)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Retrieves all current(neither dead nor transferred out) patients missing
4
+ # any demographics or haven't had their demographics updated since the
5
+ # given date
6
+ module MalawiHivProgramReports
7
+ module Clinic
8
+ class PatientsWithOutdatedDemographics
9
+ attr_reader :date, :variant
10
+
11
+ def initialize(start_date:, variant: 'poc', **_kwargs)
12
+ @date = start_date
13
+ @variant = parse_variant(variant)
14
+ end
15
+
16
+ def find_report
17
+ patients = if variant == 'poc'
18
+ poc_patients_missing_demographics
19
+ else
20
+ emastercard_patients_missing_demographics
21
+ end
22
+
23
+ patients.collect do |patient|
24
+ {
25
+ patient_id: patient.patient_id,
26
+ arv_number: patient.arv_number,
27
+ given_name: patient.given_name,
28
+ family_name: patient.family_name,
29
+ birthdate: patient[:birthdate],
30
+ gender: patient[:gender],
31
+ current_village: patient.current_village,
32
+ current_traditional_authority: patient.current_traditional_authority,
33
+ current_district: patient.current_district,
34
+ home_village: patient.home_village,
35
+ home_traditional_authority: patient.home_traditional_authority,
36
+ home_district: patient.home_district,
37
+ landmark: patient.landmark,
38
+ address_last_updated_date: patient.address_last_updated_date
39
+ }
40
+ end
41
+ end
42
+
43
+ # Find all patients missing any of the following variables:
44
+ # - birthdate
45
+ # - gender
46
+ # - name
47
+ # - address
48
+ def poc_patients_missing_demographics
49
+ quoted_date = ActiveRecord::Base.connection.quote(date)
50
+
51
+ ::Patient.find_by_sql(
52
+ <<~SQL
53
+ SELECT patient.patient_id AS patient_id,
54
+ patient_identifier.identifier AS arv_number,
55
+ person_name.given_name AS given_name,
56
+ person_name.family_name AS family_name,
57
+ person.birthdate AS birthdate,
58
+ person.gender AS gender,
59
+ person_address.city_village AS current_village,
60
+ person_address.township_division AS current_traditional_authority,
61
+ person_address.state_province AS current_district,
62
+ person_address.neighborhood_cell AS home_village,
63
+ person_address.county_district AS home_traditional_authority,
64
+ person_address.address2 AS home_district,
65
+ landmark.value AS landmark,
66
+ person_address.date_created AS address_last_updated_date
67
+ FROM patient
68
+ INNER JOIN person ON person.person_id = patient.patient_id AND person.voided = 0
69
+ INNER JOIN patient_program ON patient_program.patient_id = patient.patient_id AND patient_program.program_id = 1
70
+ INNER JOIN patient_state ON patient_state.patient_program_id = patient_program.patient_program_id
71
+ LEFT JOIN patient_identifier ON patient_identifier.patient_id = patient.patient_id
72
+ AND patient_identifier.voided = 0 AND patient_identifier.identifier_type = #{arv_number_id}
73
+ LEFT JOIN person_name ON person_name.person_id = patient.patient_id AND person_name.voided = 0
74
+ LEFT JOIN person_address ON person_address.person_id = patient.patient_id AND person_address.voided = 0
75
+ LEFT JOIN person_attribute landmark ON landmark.person_id = patient.patient_id
76
+ AND landmark.person_attribute_type_id = #{landmark_attribute_type_id}
77
+ WHERE patient.voided = 0
78
+ AND (patient_state.state != #{died_state_id}
79
+ OR (patient_state.state = #{transferred_out_state_id} AND patient_state.start_date < #{quoted_date}
80
+ AND (patient_state.end_date IS NULL OR patient_state.end_date > #{quoted_date})))
81
+ AND ((person.birthdate IS NULL OR TRIM(COALESCE(person.gender, '')) = '')
82
+ OR (TRIM(COALESCE(person_name.given_name, '')) = '' OR TRIM(COALESCE(person_name.family_name, '')) = '')
83
+ OR (TRIM(COALESCE(person_address.city_village, '')) = ''
84
+ OR TRIM(COALESCE(person_address.township_division, '')) = ''
85
+ OR TRIM(COALESCE(person_address.state_province, '')) = ''
86
+ OR TRIM(COALESCE(person_address.neighborhood_cell, '') = '')
87
+ OR TRIM(COALESCE(person_address.county_district, '') = '')
88
+ OR TRIM(COALESCE(person_address.address2, '') = '')
89
+ OR person_address.date_created < #{quoted_date}))
90
+ GROUP BY patient.patient_id
91
+ SQL
92
+ )
93
+ end
94
+
95
+ # Find all patients missing any of the following variables:
96
+ # - birthdate
97
+ # - gender
98
+ # - name
99
+ # - landmark
100
+ #
101
+ # NOTE: Old emastercard application did not collect a detailed and
102
+ # properly formatted address. The address was collected as free text and
103
+ # there was no standard provided for the address thus it's treated as
104
+ # a landmark by this system. Even for the new eMastercard application
105
+ # running atop this API, a free text alternative is provided if the
106
+ def emastercard_patients_missing_demographics
107
+ quoted_date = ActiveRecord::Base.connection.quote(date.to_s)
108
+
109
+ ::Patient.find_by_sql(
110
+ <<~SQL
111
+ SELECT patient.patient_id AS patient_id,
112
+ patient_identifier.identifier AS arv_number,
113
+ person_name.given_name AS given_name,
114
+ person_name.family_name AS family_name,
115
+ person.birthdate AS birthdate,
116
+ person.gender AS gender,
117
+ person_address.city_village AS current_village,
118
+ person_address.township_division AS current_traditional_authority,
119
+ person_address.state_province AS current_district,
120
+ person_address.neighborhood_cell AS home_village,
121
+ person_address.county_district AS home_traditional_authority,
122
+ person_address.address2 AS home_district,
123
+ landmark.value AS landmark,
124
+ person_address.date_created AS address_last_updated_date
125
+ FROM patient
126
+ INNER JOIN person ON person.person_id = patient.patient_id AND person.voided = 0
127
+ INNER JOIN patient_program ON patient_program.patient_id = patient.patient_id AND patient_program.program_id = 1
128
+ INNER JOIN patient_state ON patient_state.patient_program_id = patient_program.patient_program_id
129
+ LEFT JOIN patient_identifier ON patient_identifier.patient_id = patient.patient_id
130
+ AND patient_identifier.voided = 0 AND patient_identifier.identifier_type = #{arv_number_id}
131
+ LEFT JOIN person_name ON person_name.person_id = patient.patient_id AND person_name.voided = 0
132
+ LEFT JOIN person_address ON person_address.person_id = patient.patient_id AND person_address.voided = 0
133
+ LEFT JOIN person_attribute landmark ON landmark.person_id = patient.patient_id
134
+ AND landmark.person_attribute_type_id = #{landmark_attribute_type_id}
135
+ WHERE patient.voided = 0
136
+ AND (patient_state.state != #{died_state_id}
137
+ OR (patient_state.state = #{transferred_out_state_id} AND patient_state.start_date < #{quoted_date}
138
+ AND (patient_state.end_date IS NULL OR patient_state.end_date > #{quoted_date})))
139
+ AND ((person.birthdate IS NULL OR TRIM(COALESCE(person.gender, '')) = '')
140
+ OR (TRIM(COALESCE(person_name.given_name, '')) = '' OR TRIM(COALESCE(person_name.family_name, '')) = '')
141
+ OR (TRIM(COALESCE(landmark.value, '')) = '' OR landmark.date_created < #{quoted_date}))
142
+ GROUP BY patient.patient_id
143
+ SQL
144
+ )
145
+ end
146
+
147
+ def arv_number_id
148
+ @arv_number_id ||= ::PatientIdentifierType.find_by(name: 'ARV Number').patient_identifier_type_id
149
+ end
150
+
151
+ def died_state_id
152
+ @died_state_id ||= ::ProgramWorkflowState.find_by_name_and_program(name: 'Died', program_id: 1).id
153
+ end
154
+
155
+ def transferred_out_state_id
156
+ @transferred_out_state_id ||= ::ProgramWorkflowState.find_by_name_and_program(name: 'Patient transferred out',
157
+ program_id: 1).id
158
+ end
159
+
160
+ def landmark_attribute_type_id
161
+ @landmark_attribute_type_id ||= ::PersonAttributeType.find_by_name('Landmark Or Plot Number').id
162
+ end
163
+
164
+ def parse_variant(variant)
165
+ variant = variant.downcase
166
+
167
+ return variant if %w[poc emastercard].include?(variant)
168
+
169
+ raise ::InvalidParameterError, "Invalid report variant '#{variant}'; expected poc or emastercard"
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Retrieve all pregnant females in a given time period
4
+ module MalawiHivProgramReports
5
+ module Clinic
6
+ class PregnantPatients
7
+ attr_reader :start_date, :end_date
8
+
9
+ include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
10
+
11
+ def initialize(start_date:, end_date:, **kwargs)
12
+ @start_date = start_date
13
+ @end_date = end_date
14
+ @occupation = kwargs[:occupation]
15
+ end
16
+
17
+ def find_report
18
+ all_pregnant.map do |patient|
19
+ {
20
+ patient_id: patient.patient_id,
21
+ arv_number: patient.arv_number,
22
+ given_name: patient.given_name,
23
+ family_name: patient.family_name,
24
+ gender: patient.gender,
25
+ birthdate: patient.birthdate,
26
+ last_reported_date: patient.last_reported_date
27
+ }
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def all_pregnant
34
+ str_start_date = ActiveRecord::Base.connection.quote(start_date)
35
+ str_end_date = ActiveRecord::Base.connection.quote(end_date)
36
+
37
+ ::Person.find_by_sql(
38
+ <<~SQL
39
+ SELECT obs.person_id AS patient_id,
40
+ patient_identifier.identifier AS arv_number,
41
+ given_name,
42
+ family_name,
43
+ gender,
44
+ birthdate,
45
+ obs.obs_datetime AS last_reported_date
46
+ FROM obs
47
+ INNER JOIN person ON person.person_id = obs.person_id
48
+ INNER JOIN person_name ON person_name.person_id = obs.person_id
49
+ LEFT JOIN patient_identifier ON patient_identifier.patient_id = obs.person_id
50
+ AND patient_identifier.identifier_type = #{arv_number_type_id}
51
+ INNER JOIN encounter ON encounter.encounter_id = obs.encounter_id
52
+ AND encounter.program_id = #{hiv_program_id}
53
+ LEFT JOIN (#{current_occupation_query}) a ON a.person_id = obs.person_id
54
+ WHERE obs.concept_id IN (#{pregnant_concepts.select(:concept_id).to_sql})
55
+ AND obs.value_coded = #{yes_concept_id}
56
+ AND obs.person_id IN (#{patients_on_treatment.to_sql})
57
+ AND obs.obs_datetime = (
58
+ SELECT MAX(obs_date.obs_datetime) FROM obs obs_date
59
+ INNER JOIN encounter USING (encounter_id)
60
+ WHERE obs_date.concept_id IN (#{pregnant_concepts.select(:concept_id).to_sql})
61
+ AND DATE(obs_date.obs_datetime) BETWEEN #{str_start_date} AND #{str_end_date}
62
+ AND obs_date.person_id = obs.person_id
63
+ AND program_id = #{hiv_program_id}
64
+ ) #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
65
+ GROUP BY obs.person_id
66
+ SQL
67
+ )
68
+ end
69
+
70
+ def patients_on_treatment
71
+ PatientsOnTreatment.within(start_date, end_date)
72
+ end
73
+
74
+ def pregnant_concepts
75
+ ::ConceptName.where(name: ['Is patient pregnant?', 'patient_pregnant'])
76
+ end
77
+
78
+ def yes_concept_id
79
+ ::ConceptName.find_by_name('Yes').concept_id
80
+ end
81
+
82
+ def arv_number_type_id
83
+ ::PatientIdentifierType.find_by_name('ARV Number').id
84
+ end
85
+
86
+ def hiv_program_id
87
+ ::ArtService::Constants::PROGRAM_ID
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ # this report is used to generate regimen data
4
+ module MalawiHivProgramReports
5
+ module Clinic
6
+ class RegimenDispensationData
7
+ attr_reader :start_date, :end_date, :type
8
+ include MalawiHivProgramReports::Utils::CommonSqlQueryUtils
9
+ include MalawiHivProgramReports::Adapters::Moh::Custom
10
+ include Utils
11
+
12
+ def initialize(start_date:, end_date:, **kwargs)
13
+ @start_date = ActiveRecord::Base.connection.quote(start_date.to_date.strftime('%Y-%m-%d 00:00:00'))
14
+ @end_date = ActiveRecord::Base.connection.quote(end_date.to_date.strftime('%Y-%m-%d 23:59:59'))
15
+ @type = kwargs[:type]
16
+ @occupation = kwargs[:occupation]
17
+ end
18
+
19
+ def find_report
20
+ drop_regimen_data
21
+ create_filtered_data
22
+ process_clients
23
+ end
24
+
25
+ private
26
+
27
+ # rubocop:disable Metrics/AbcSize
28
+ def drop_regimen_data
29
+ ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_current_dispensation'
30
+ ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_drug_dispensed'
31
+ ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_current_regimen_names'
32
+ ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_current_patient_regimen'
33
+ ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_reg_outcome'
34
+ ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_vl_results'
35
+ ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_current_vl_results'
36
+ ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_regimen_patient_weight'
37
+ ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_regimen_data'
38
+ ActiveRecord::Base.connection.execute 'DROP TABLE IF EXISTS temp_patient_start_date'
39
+ end
40
+ # rubocop:enable Metrics/AbcSize
41
+
42
+ def create_filtered_data
43
+ create_temp_current_dispensation
44
+ create_temp_drug_dispensed
45
+ create_temp_current_regimen
46
+ create_temp_current_patient_regimen
47
+ create_temp_patient_outcome
48
+ create_temp_vl_result
49
+ create_temp_current_vl_results
50
+ create_temp_regimen_patient_weight
51
+ create_temp_patient_start_date
52
+ end
53
+
54
+ def create_temp_current_dispensation
55
+ ActiveRecord::Base.connection.execute <<~SQL
56
+ CREATE TABLE temp_current_dispensation
57
+ SELECT o.patient_id, MAX(o.start_date) AS start_date
58
+ FROM orders o
59
+ INNER JOIN drug_order od ON od.order_id = o.order_id AND od.quantity > 0 AND od.drug_inventory_id IN (SELECT drug_id FROM arv_drug)
60
+ LEFT JOIN (#{current_occupation_query}) a ON a.person_id = o.patient_id
61
+ WHERE o.start_date > #{end_date} - INTERVAL 18 MONTH AND o.start_date < #{end_date} + INTERVAL 1 DAY
62
+ AND o.voided = 0 #{%w[Military Civilian].include?(@occupation) ? 'AND' : ''} #{occupation_filter(occupation: @occupation, field_name: 'value', table_name: 'a', include_clause: false)}
63
+ AND o.order_type_id = 1 -- drug order
64
+ GROUP BY o.patient_id
65
+ SQL
66
+ ActiveRecord::Base.connection.execute 'create index current_disp on temp_current_dispensation (patient_id, start_date)'
67
+ end
68
+
69
+ def create_temp_drug_dispensed
70
+ ActiveRecord::Base.connection.execute <<~SQL
71
+ CREATE table temp_drug_dispensed
72
+ SELECT o.patient_id, od.drug_inventory_id, d.name, o.start_date, od.quantity
73
+ FROM orders o
74
+ INNER JOIN temp_current_dispensation tcd ON tcd.patient_id = o.patient_id AND tcd.start_date = o.start_date
75
+ INNER JOIN drug_order od ON od.order_id = o.order_id AND od.quantity > 0 AND od.drug_inventory_id IN (SELECT drug_id FROM arv_drug)
76
+ INNER JOIN drug d ON d.drug_id = od.drug_inventory_id AND d.retired = 0
77
+ WHERE o.voided = 0
78
+ AND o.order_type_id = 1 -- drug order
79
+ SQL
80
+ ActiveRecord::Base.connection.execute 'create index drug_disp on temp_drug_dispensed (patient_id, start_date)'
81
+ end
82
+
83
+ def create_temp_current_regimen
84
+ ActiveRecord::Base.connection.execute <<~SQL
85
+ CREATE table temp_current_regimen_names
86
+ SELECT GROUP_CONCAT(drug.drug_id ORDER BY drug.drug_id ASC) AS drugs, regimen_name.name AS name
87
+ FROM moh_regimen_combination AS combo
88
+ INNER JOIN moh_regimen_combination_drug AS drug USING (regimen_combination_id)
89
+ INNER JOIN moh_regimen_name AS regimen_name USING (regimen_name_id)
90
+ GROUP BY combo.regimen_combination_id
91
+ SQL
92
+ ActiveRecord::Base.connection.execute 'create index regimen_names on temp_current_regimen_names (drugs(50))'
93
+ end
94
+
95
+ def create_temp_current_patient_regimen
96
+ ActiveRecord::Base.connection.execute <<~SQL
97
+ CREATE table temp_current_patient_regimen
98
+ SELECT patient_id, name
99
+ FROM temp_current_regimen_names tcrn
100
+ INNER JOIN (
101
+ SELECT patient_id, GROUP_CONCAT(drug_inventory_id ORDER BY drug_inventory_id ASC) AS drugs
102
+ FROM temp_drug_dispensed
103
+ GROUP BY patient_id
104
+ ) d ON d.drugs = tcrn.drugs
105
+ SQL
106
+ ActiveRecord::Base.connection.execute 'create index patient_regimen on temp_current_patient_regimen (patient_id)'
107
+ end
108
+
109
+ def create_temp_patient_outcome
110
+ ActiveRecord::Base.connection.execute <<~SQL
111
+ CREATE TABLE temp_reg_outcome
112
+ SELECT patient_id, #{type == 'pepfar' ? "pepfar_patient_outcome(patient_id, #{end_date})" : "patient_outcome(patient_id, #{end_date})"} outcome
113
+ FROM temp_current_dispensation
114
+ SQL
115
+ ActiveRecord::Base.connection.execute 'create index reg_outcome on temp_reg_outcome (patient_id)'
116
+ end
117
+
118
+ def create_temp_vl_result
119
+ ActiveRecord::Base.connection.execute <<~SQL
120
+ CREATE table temp_vl_results
121
+ SELECT
122
+ lab_result_obs.obs_datetime AS result_date,
123
+ CONCAT (COALESCE(measure.value_modifier, '='),' ',COALESCE(measure.value_numeric, measure.value_text, '')) AS result,
124
+ lab_result_obs.person_id AS patient_id
125
+ FROM obs AS lab_result_obs
126
+ INNER JOIN orders ON orders.order_id = lab_result_obs.order_id AND orders.voided = 0
127
+ INNER JOIN obs AS measure ON measure.obs_group_id = lab_result_obs.obs_id AND measure.voided = 0
128
+ INNER JOIN (
129
+ SELECT concept_id, name
130
+ FROM concept_name
131
+ INNER JOIN concept USING (concept_id)
132
+ WHERE concept.retired = 0
133
+ AND name NOT LIKE 'Lab test result'
134
+ GROUP BY concept_id
135
+ ) AS measure_concept ON measure_concept.concept_id = measure.concept_id
136
+ WHERE lab_result_obs.voided = 0
137
+ AND measure.person_id IN (SELECT patient_id FROM temp_reg_outcome WHERE outcome = 'On antiretrovirals')
138
+ AND (measure.value_numeric IS NOT NULL || measure.value_text IS NOT NULL)
139
+ AND lab_result_obs.obs_datetime < #{end_date} + INTERVAL 1 DAY
140
+ ORDER BY lab_result_obs.obs_datetime DESC
141
+ SQL
142
+ ActiveRecord::Base.connection.execute 'create index vl_result on temp_vl_results (patient_id, result_date)'
143
+ end
144
+
145
+ def create_temp_current_vl_results
146
+ ActiveRecord::Base.connection.execute <<~SQL
147
+ CREATE TABLE temp_current_vl_results
148
+ SELECT t.*
149
+ FROM temp_vl_results t
150
+ LEFT JOIN temp_vl_results td ON td.patient_id = t.patient_id AND td.result_date > t.result_date
151
+ WHERE td.patient_id IS NULL
152
+ SQL
153
+ ActiveRecord::Base.connection.execute 'create index current_vl on temp_current_vl_results (patient_id)'
154
+ end
155
+
156
+ def create_temp_regimen_patient_weight
157
+ ActiveRecord::Base.connection.execute <<~SQL
158
+ CREATE TABLE temp_regimen_patient_weight
159
+ SELECT tro.patient_id, o.value_numeric AS weight
160
+ FROM temp_reg_outcome tro
161
+ INNER JOIN (
162
+ SELECT person_id, MAX(obs_datetime) AS max_datetime
163
+ FROM obs
164
+ WHERE concept_id = 5089 AND voided = 0 AND obs_datetime < #{end_date} + INTERVAL 1 DAY
165
+ GROUP BY person_id
166
+ ) latest_obs ON latest_obs.person_id = tro.patient_id
167
+ INNER JOIN obs o ON o.person_id = latest_obs.person_id AND o.concept_id = 5089 AND o.obs_datetime = latest_obs.max_datetime AND o.voided = 0 AND o.obs_datetime < #{end_date} + INTERVAL 1 DAY
168
+ WHERE tro.outcome = 'On antiretrovirals'
169
+ SQL
170
+ ActiveRecord::Base.connection.execute 'create index patient_weight on temp_regimen_patient_weight (patient_id)'
171
+ end
172
+
173
+ def create_temp_patient_start_date
174
+ ActiveRecord::Base.connection.execute <<~SQL
175
+ CREATE TABLE temp_patient_start_date
176
+ SELECT
177
+ `p`.`patient_id` AS `patient_id`,
178
+ cast(patient_date_enrolled(`p`.`patient_id`) as date) AS `date_enrolled`,
179
+ date_antiretrovirals_started(`p`.`patient_id`, min(`s`.`start_date`)) AS `earliest_start_date`
180
+ FROM
181
+ ((`patient_program` `p`
182
+ LEFT JOIN `person` `pe` ON ((`pe`.`person_id` = `p`.`patient_id`))
183
+ LEFT JOIN `patient_state` `s` ON ((`p`.`patient_program_id` = `s`.`patient_program_id`)))
184
+ LEFT JOIN `person` ON ((`person`.`person_id` = `p`.`patient_id`)))
185
+ WHERE
186
+ ((`p`.`voided` = 0)
187
+ AND (`s`.`voided` = 0)
188
+ AND (`p`.`program_id` = 1)
189
+ AND (`s`.`state` = 7))
190
+ AND (`s`.`start_date` < #{end_date} + INTERVAL 1 DAY)
191
+ AND p.patient_id IN (SELECT patient_id FROM temp_reg_outcome WHERE outcome = 'On antiretrovirals')
192
+ GROUP BY `p`.`patient_id`;
193
+ SQL
194
+ ActiveRecord::Base.connection.execute 'create index start_date on temp_patient_start_date (patient_id)'
195
+ end
196
+
197
+ def clients_alive_on_treatment
198
+ ActiveRecord::Base.connection.select_all <<~SQL
199
+ SELECT
200
+ trc.patient_id,
201
+ p.gender,
202
+ p.birthdate,
203
+ pn.given_name,
204
+ pn.family_name,
205
+ i.identifier arv_number,
206
+ tpw.weight,
207
+ tcvr.result_date,
208
+ tcvr.result,
209
+ tcp.name regimen,
210
+ trc.earliest_start_date
211
+ FROM temp_patient_start_date trc
212
+ INNER JOIN temp_current_dispensation tcd ON tcd.patient_id = trc.patient_id AND tcd.start_date BETWEEN #{@start_date} AND #{@end_date}
213
+ INNER JOIN person p ON p.person_id = trc.patient_id AND p.voided = 0
214
+ INNER JOIN person_name pn ON pn.person_id = p.person_id AND pn.voided = 0
215
+ LEFT JOIN patient_identifier i ON i.patient_id = p.person_id AND i.identifier_type = 4 AND i.voided = 0
216
+ LEFT JOIN temp_regimen_patient_weight tpw ON tpw.patient_id = trc.patient_id
217
+ LEFT JOIN temp_current_vl_results tcvr ON tcvr.patient_id = trc.patient_id
218
+ LEFT JOIN temp_current_patient_regimen tcp ON tcp.patient_id = trc.patient_id
219
+ GROUP BY trc.patient_id
220
+ SQL
221
+ end
222
+
223
+ def alive_clients
224
+ clients = ActiveRecord::Base.connection.select_all <<~SQL
225
+ SELECT trc.patient_id
226
+ FROM temp_reg_outcome trc
227
+ INNER JOIN person p ON p.person_id = trc.patient_id AND p.voided = 0
228
+ where trc.outcome = 'On antiretrovirals' AND LEFT(p.gender, 1) = 'F'
229
+ SQL
230
+ clients.map { |client| client['patient_id'] }
231
+ end
232
+
233
+ def process_clients
234
+ clients = {}
235
+ @maternal_status = maternal_status
236
+ clients_alive_on_treatment.each do |client|
237
+ clients[client['patient_id']] = {
238
+ arv_number: client['arv_number'],
239
+ given_name: client['given_name'],
240
+ family_name: client['family_name'],
241
+ birthdate: client['birthdate'],
242
+ gender: client['gender'] == 'M' ? 'M' : fetch_maternal_status(client['patient_id']),
243
+ current_regimen: client['regimen'],
244
+ current_weight: client['weight'],
245
+ art_start_date: client['earliest_start_date'],
246
+ medication: fetch_medication(client['patient_id']),
247
+ vl_result: client['result'],
248
+ vl_result_date: client['result_date']
249
+ }
250
+ end
251
+ clients
252
+ end
253
+
254
+ def maternal_status
255
+ MalawiHivProgramReports::Pepfar::ViralLoadCoverage2.new(start_date: @start_date,
256
+ end_date: @end_date).vl_maternal_status(alive_clients)
257
+ end
258
+
259
+ def fetch_maternal_status(patient_id)
260
+ return nil if patient_id.blank?
261
+
262
+ gender = 'FNP'
263
+ gender = 'FP' if @maternal_status[:FP].include?(patient_id)
264
+ gender = 'FBf' if @maternal_status[:FBf].include?(patient_id)
265
+ gender
266
+ end
267
+
268
+ def fetch_medication(patient_id)
269
+ result = []
270
+ data = ActiveRecord::Base.connection.select_all <<~SQL
271
+ SELECT tdd.name, DATE(tdd.start_date) start_date, tdd.quantity
272
+ FROM temp_drug_dispensed tdd
273
+ WHERE tdd.patient_id = #{patient_id}
274
+ SQL
275
+ data.each do |row|
276
+ result << { medication: row['name'], start_date: row['start_date'], quantity: row['quantity'] }
277
+ end
278
+ result
279
+ end
280
+ end
281
+ end
282
+ end