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,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