Skip to content

Commit bced482

Browse files
authored
feat(OpenSRP Exporter): Selective Exporting (#5684)
**Story card:** [sc-16544](https://app.shortcut.com/simpledotorg/story/16544/opensrp-selective-exporting) ## Because To reduce impact on data, we need to enable differential exports. ## This addresses - Refactoring the `opensrp:export` task into a one-off service - Testing that service - Filtering selected patients for export [probably from config file] ## Test instructions suite tests
1 parent d3acaf6 commit bced482

File tree

2 files changed

+396
-0
lines changed

2 files changed

+396
-0
lines changed

app/services/one_off/opensrp/exporter.rb

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,263 @@ module Opensrp
44
#
55
# Base class which acts as an entry point for the rake task.
66
class Exporter
7+
class Config
8+
attr_reader :report_start, :report_end, :facilities, :time_window, :patients
9+
10+
def initialize config_file
11+
data = YAML.load_file(config_file).deep_symbolize_keys.with_indifferent_access
12+
time_boundaries = data[:time_boundaries]
13+
14+
@report_start = if has_report_start?(data)
15+
time_boundaries[:report_start]
16+
else
17+
DateTime.parse("2020-01-01")
18+
end
19+
@report_end = if has_report_end?(data)
20+
time_boundaries[:report_end]
21+
else
22+
DateTime.now
23+
end
24+
@facilities = data[:facilities]
25+
@time_bound = using_time_boundaries? data
26+
@time_window = @report_start..@report_end
27+
@patients = get_patients data
28+
end
29+
30+
def time_bound?
31+
@time_bound || false
32+
end
33+
34+
def using_time_boundaries?(config)
35+
config.has_key? :time_boundaries
36+
end
37+
38+
def has_report_start?(config)
39+
using_time_boundaries?(config) && config[:time_boundaries].has_key?(:report_start)
40+
end
41+
42+
def has_report_end?(config)
43+
using_time_boundaries?(config) && config[:time_boundaries].has_key?(:report_end)
44+
end
45+
46+
def get_patients config
47+
config[:patients] || []
48+
end
49+
end
50+
51+
attr_reader :tally
52+
753
def self.export config, output
854
new(config, output).call!
955
end
56+
57+
def initialize config_file, output_file, logger: nil
58+
raise "Config file should be YAML" unless %w[yaml yml].include?(config_file.split(".").last)
59+
raise "Output file should be JSON" unless output_file.split(".").last == "json"
60+
61+
if logger.nil?
62+
initialize_logger
63+
else
64+
@logger = logger
65+
end
66+
67+
@config = Config.new config_file
68+
@logger.info "Exporting data using config at #{config_file}"
69+
@output = output_file
70+
@resources = []
71+
@encounters = []
72+
@tally = Hash.new(0)
73+
end
74+
75+
def initialize_logger
76+
logfile = Rails.root.join("log", "#{Rails.env}.log")
77+
@logger = ActiveSupport::Logger.new(logfile)
78+
@logger.extend(ActiveSupport::Logger.broadcast(ActiveSupport::Logger.new($stdout)))
79+
end
80+
81+
def call!
82+
@logger.info "Time Boundaries: [#{@config.report_start}..#{@config.report_end}]"
83+
84+
patients = select_patients from_facilities: @config.facilities.keys
85+
patients.each do |patient|
86+
export_patient_details patient
87+
export_blood_pressure_details patient
88+
export_blood_sugar_details patient
89+
export_prescription_drugs_details patient
90+
export_appointments_details patient
91+
export_medical_history_details patient
92+
end
93+
94+
@tally[:encounters] += @encounters.size
95+
@resources << OneOff::Opensrp::EncounterGenerator.new(@encounters).generate
96+
97+
write_audit_trail patients
98+
end
99+
100+
def select_patients from_facilities: []
101+
raise "No facility selected for export" if from_facilities.empty?
102+
103+
relation = Patient.where(assigned_facility_id: from_facilities)
104+
if @config.patients.empty?
105+
relation
106+
else
107+
relation.where(id: @config.patients)
108+
end
109+
end
110+
111+
def export_patient_details patient
112+
return unless @config.time_window.cover?(patient.recorded_at)
113+
114+
patient_exporter = OneOff::Opensrp::PatientExporter.new(patient, @config.facilities)
115+
@resources << patient_exporter.export
116+
@tally[:patients] += 1
117+
118+
@resources << patient_exporter.export_registration_questionnaire_response
119+
@tally[:questionnaire_response] += 1
120+
121+
@encounters << patient_exporter.export_registration_encounter
122+
end
123+
124+
def export_blood_pressure_details patient
125+
blood_pressures = if @config.time_bound?
126+
patient
127+
.blood_pressures
128+
.where(recorded_at: @config.time_window)
129+
.or(patient
130+
.blood_pressures
131+
.where(updated_at: @config.time_window))
132+
else
133+
patient.blood_pressures
134+
end
135+
@logger.debug "Patient[##{patient.id}] has #{blood_pressures.size} blood pressure readings."
136+
@tally[:observation] += blood_pressures.size
137+
blood_pressures.each do |bp|
138+
# This is technically an FHIR::Observation, with code set to a blood pressure code
139+
bp_exporter = OneOff::Opensrp::BloodPressureExporter.new(bp, @config.facilities)
140+
@resources << bp_exporter.export
141+
@encounters << bp_exporter.export_encounter
142+
end
143+
end
144+
145+
def export_blood_sugar_details patient
146+
blood_sugars = if @config.time_bound?
147+
patient
148+
.blood_sugars
149+
.where(recorded_at: @config.time_window)
150+
.or(patient
151+
.blood_sugars
152+
.where(updated_at: @config.time_window))
153+
else
154+
patient.blood_sugars
155+
end
156+
@logger.debug "Patient[##{patient.id}] has #{blood_sugars.size} blood sugar readings."
157+
@tally[:observation] += blood_sugars.size
158+
blood_sugars.each do |bs|
159+
# This is technically an FHIR::Observation, with code set to a blood sugar code
160+
bs_exporter = OneOff::Opensrp::BloodSugarExporter.new(bs, @config.facilities)
161+
if patient.medical_history.diabetes_no?
162+
@resources << bs_exporter.export_no_diabetes_observation
163+
end
164+
@resources << bs_exporter.export
165+
@encounters << bs_exporter.export_encounter
166+
end
167+
end
168+
169+
def export_prescription_drugs_details patient
170+
prescription_drugs = if @config.time_bound?
171+
patient
172+
.prescription_drugs
173+
.where(created_at: @config.time_window)
174+
.or(patient
175+
.prescription_drugs
176+
.where(updated_at: @config.time_window))
177+
else
178+
patient.prescription_drugs
179+
end
180+
@logger.debug "Patient[##{patient.id}] has #{prescription_drugs.size} drugs prescribed."
181+
@tally[:flags] += prescription_drugs.size
182+
prescription_drugs.each do |drug|
183+
drug_exporter = OneOff::Opensrp::PrescriptionDrugExporter.new(drug, @config.facilities)
184+
@resources << drug_exporter.export_dosage_flag
185+
@encounters << drug_exporter.export_encounter
186+
end
187+
end
188+
189+
def export_appointments_details patient
190+
appointments = if @config.time_bound?
191+
patient
192+
.appointments
193+
.where(created_at: @config.time_window)
194+
.or(patient
195+
.appointments
196+
.where(updated_at: @config.time_window))
197+
else
198+
patient.appointments
199+
end
200+
@logger.debug "Patient[##{patient.id}] has #{appointments.size} appointments."
201+
@tally[:appointments] += appointments.size
202+
@tally[:tasks] += appointments.includes(:call_results).where.not(call_results: {id: nil}).size
203+
@tally[:flags] += appointments.includes(:call_results).where.not(call_results: {id: nil}).size
204+
appointments.each do |appointment|
205+
appointment_exporter = OneOff::Opensrp::AppointmentExporter.new(appointment, @config.facilities)
206+
@resources << appointment_exporter.export
207+
if appointment.call_results.present?
208+
@resources << appointment_exporter.export_call_outcome_task
209+
@resources << appointment_exporter.export_call_outcome_flag
210+
end
211+
@encounters << appointment_exporter.export_encounter
212+
end
213+
end
214+
215+
def export_medical_history_details patient
216+
OneOff::Opensrp::MedicalHistoryExporter.new(patient.medical_history, @config.facilities).then do |medical_history_exporter|
217+
@resources << medical_history_exporter.export
218+
@encounters << medical_history_exporter.export_encounter
219+
end
220+
@tally[:conditions] += 1
221+
end
222+
223+
def write_audit_trail patients
224+
CSV.open("audit_trail.csv", "w") do |csv|
225+
csv << create_audit_record(@config.facilities, patients.first).keys
226+
patients.each do |patient|
227+
csv << create_audit_record(@config.facilities, patient).values
228+
end
229+
end
230+
end
231+
232+
def create_audit_record(facilities, patient)
233+
return {} if patient.nil?
234+
235+
{
236+
patient_id: patient.id,
237+
sri_lanka_personal_health_number: patient.business_identifiers.where(identifier_type: "sri_lanka_personal_health_number")&.first&.identifier,
238+
patient_bp_passport_number: patient.business_identifiers.where(identifier_type: "simple_bp_passport")&.first&.identifier,
239+
patient_name: patient.full_name,
240+
patient_gender: patient.gender,
241+
patient_date_of_birth: patient.date_of_birth || patient.age_updated_at - patient.age.years,
242+
patient_address: patient.address ? patient.address.street_address : "",
243+
patient_telephone: patient.phone_numbers.pluck(:number).join(";"),
244+
patient_facility: facilities[patient.assigned_facility_id][:name],
245+
patient_preferred_language: "Sinhala",
246+
patient_active: patient.status_active?,
247+
patient_deceased: patient.status_dead?,
248+
condition: ("HTN" if patient.medical_history.hypertension_yes?) || ("DM" if patient.medical_history.diabetes_yes?),
249+
blood_pressure: patient.latest_blood_pressure&.values_at(:systolic, :diastolic)&.join("/"),
250+
bmi: nil,
251+
appointment_date: patient.appointments.order(device_updated_at: :desc).where(status: "scheduled")&.first&.device_updated_at&.to_date&.iso8601,
252+
medication: patient.prescription_drugs.order(device_updated_at: :desc).where(is_deleted: false)&.first&.values_at(:name, :dosage)&.join(" "),
253+
glucose_measure: patient.latest_blood_sugar&.blood_sugar_value.then { |bs| "%.2f" % bs if bs },
254+
glucose_measure_type: patient.latest_blood_sugar&.blood_sugar_type,
255+
call_outcome: patient.appointments.order(device_updated_at: :desc)&.first&.call_results&.order(device_created_at: :desc)&.first&.result_type
256+
}
257+
end
258+
259+
private
260+
261+
def read config_file
262+
YAML.load_file(config_file).deep_symbolize_keys.with_indifferent_access
263+
end
10264
end
11265
end
12266
end

0 commit comments

Comments
 (0)