-
Notifications
You must be signed in to change notification settings - Fork 26
/
oncore_endpoint_controller.rb
307 lines (255 loc) · 13.2 KB
/
oncore_endpoint_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# Copyright © 2011-2020 MUSC Foundation for Research Development
# All rights reserved.
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
# SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
class OncoreEndpointController < ApplicationController
# All of the following nested classes are used in order to avoid a duplicate key error from WashOut.
# If args contains an element within the same element, there will be an error when generating the WSDL.
# For example:
# This one gives a dupicate error | This one DOES NOT result in a duplicate error
# :args => { | :args => {
# :element => { | :element => {
# :thing => :string, | :thing => :string,
# :element => { :@attribute => :string } | :element => Element #Element is a custom WashOut::Type class
# } | }
# } | }
class Id < WashOut::Type
map :@extension => :string, :@root => :string
end
class Code < WashOut::Type
map :@code => :string, :@codeSystem => :string
end
class EffectiveTime < WashOut::Type
map :low => { :@value => :string },
:high => { :@value => :string }
end
class SequenceNumber < WashOut::Type
map :@value => :string
end
class TPED < WashOut::Type
#TPED = TimePointEventDefinition
map :id => Id,
:title => :string
end
class Component1 < WashOut::Type
# Base component1 element, no other components nested inside
map :sequenceNumber => SequenceNumber,
:timePointEventDefinition => TPED
end
class Component2Procedure < WashOut::Type
# component2 elements with nested procedure element
map :procedure => {
:code => Code
}
end
class Component2Encounter < WashOut::Type
# component2 elements with nested encounter element
map :encounter => {
:effectiveTime => EffectiveTime,
:activityTime => { :@value => :string }
}
end
class Component2Arm < WashOut::Type
# component2 elements with nested arm element
map :arm => {
:id => { :@extension => :string },
:title => :string
}
end
class TPEDComponent1 < WashOut::Type
# Complex type structure for TPED with a component1 nested inside
map :id => Id,
:title => :string,
:code => Code,
:component1 => [Component1],
:component2 => Component2Procedure,
:effectiveTime => EffectiveTime
end
#############################################
# SOAP Endpoint for OnCore RPE messages #
#############################################
soap_service namespace: 'urn:ihe:qrph:rpe:2009', camelize_wsdl: :lower, parser: :nokogiri
# might need to camelize wsdl for OnCore since I'm pretty sure they use Java and camelcase
soap_action "RetrieveProtocolDefResponse",
:args => {
:protocolDef => {
:plannedStudy => {
:id => Id,
:title => :string,
:text => :string,
:subjectOf => [{
:studyCharacteristic => {
:code => Code,
:value => { :@value => :string, :@code => :string, :@codeSystem => :string }
}
}],
:component4 => [{
:timePointEventDefinition => {
:id => Id,
:title => :string,
:code => Code,
:component1 => [{
:sequenceNumber => SequenceNumber,
:timePointEventDefinition => TPEDComponent1
}],
:component2 => Component2Encounter
}
}],
:component2 => [Component2Arm]
}
}
},
:return => { 'tns:responseCode' => :string },
:header_return => :string,
:to => :retrieve_protocol_def
def retrieve_protocol_def
begin
find_valid_protocol
# Don't try to build calendar information if it doesn't exist (RPE messages don't have calendar info)
unless is_rpe_message?
# Add arms to the protocol
get_arms_from_cells
# Build out calendar info (visit groups, line items, line item visits, visits, etc.) from VISIT elements for each arm
@protocol.arms.each do |arm|
build_calendar_info(arm)
end
end
rescue Exception => e
# Print params and exception to testing log
print_params_to_log(e)
# Render a SOAP fault response for exceptions instead of the default HTML response from Rails
render_soap_error(e.message, 'soap:Server')
else
# Print params to testing log
print_params_to_log
# return PROTOCOL_RECEIVED SOAP response on successful load (they requested this solely because that's the response Epic sends)
render :soap => { 'tns:responseCode' => 'PROTOCOL_RECEIVED' },
:header => SecureRandom.uuid
end
end
private
def find_valid_protocol
id = oncore_endpoint_params[:plannedStudy][:id][:extension] #protocol ID as a string with the format "STUDY#{id}"
id.try(:slice!, "STUDY")
if !id.nil? && @protocol = Protocol.find(id)
if @protocol.has_clinical_services?
raise "Error: SPARC Protocol #{id} has an existing calendar and cannot be overwritten."
end
else
raise "Error: No existing SPARC Protocol with identifier #{id}."
end
end
# Creates arms on the protocol and
# assigns a hash with the structure { SPARC_arm_id => arm_code } since arm_code is used like an ID that isn't stored in SPARC
def get_arms_from_cells
# component4 CELL elements contain arm and visit imformation including calendar and budget version.
@arm_codes = {}
oncore_endpoint_params[:plannedStudy][:component4].select{ |c4| c4[:timePointEventDefinition][:code][:code] == "CELL" }.each do |cell|
arm_code = cell[:timePointEventDefinition][:id][:extension].split('.')[1]
arm_name = cell[:timePointEventDefinition][:title].gsub(/([^A][^r][^m][^\:])+Arm\:/, '')
# Remove bad characters from the arm name
arm_name.gsub!(/[\[\]\*\/\\\?\:]/, '')
@calendar_version = cell[:timePointEventDefinition][:title].split(/[\s\:]/)[1] # can't do this for arm name because the name can have a :
budget_version = cell[:timePointEventDefinition][:title].split(/[\s\:]/)[3]
# The number of VISITS equal the number of visit groups, we can use that for the visit count
# Visits are also listed under CELL elements but are nested under CYCLES, which have no SPARC equivalent, so VISITs are used for simplicity
visit_count = oncore_endpoint_params[:plannedStudy][:component4].select{ |c4|
c4[:timePointEventDefinition][:code][:code] == "VISIT" && c4[:timePointEventDefinition][:id][:extension].split('.').first == arm_code
}.count
if arm = @protocol.arms.create(name: arm_name, subject_count: 1, visit_count: visit_count)
@arm_codes[arm.id] = arm_code
end
end
end
def build_calendar_info(arm)
# component4 VISIT elements contain visit group information and procedures.
# VISITS are like visit groups and PROCS are like line item visits, including service information.
service_request = @protocol.service_requests.first
oncore_endpoint_params[:plannedStudy][:component4].select{ |c4|
c4[:timePointEventDefinition][:code][:code] == "VISIT" && c4[:timePointEventDefinition][:id][:extension].split('.').first == @arm_codes[arm.id]
}.each_with_index do |oncore_visit, position|
if position == 0 # procedures are on VISITs, but we only need to make line items and line items visits once per arm
# Create line items for a default placeholder service. The ID for this service is XXXXXX
service = Service.find(41714)
if service.nil?
raise "Unable to find the default OnCore Push service."
end
service_request.create_line_items_for_service(service: service)
# -------------------------------------------------------------------------------------
# TODO: get the service from the procedure.
# Code for this is temporarily removed until the Chargemaster situation can be figured out.
# This might need to be a conditional case where if there are no procedures, use the default service, otherwise, get the services.
# oncore_visit[:timePointEventDefinition][:component1].select{ |c1| c1[:timePointEventDefinition][:code][:code] == "PROC" }.each do |procedure|
# # Get the service from the procedure.
# service_name = procedure[:timePointEventDefinition][:title]
# service_code = procedure[:timePointEventDefinition][:component2][:procedure][:code][:code] # either eap_id or cpt_code
# service = Service.where(name: service_name, is_available: true).merge(Service.where(cpt_code: service_code).or(Service.where(eap_id: service_code))).first
# if service.nil?
# raise "Unable to find service #{service_name} with code #{service_code}"
# end
# # For each procedure, make a line item on the protocol
# service_request.create_line_items_for_service(service: service)
# end
# -------------------------------------------------------------------------------------
end
# create a visit group for each VISIT
# each VISIT should have one encounter. The encounter contains dates relative to Jan 1, 2000
# effectiveTime = window before and after:
# low = window before. ex) 20000327 - window before: 3
# high = window after. ex) 20000402 - window after: 3
# activityTime = day ex) 20000330 - day: 90
encounter = oncore_visit[:timePointEventDefinition][:component2][:encounter]
visit_group = arm.visit_groups[position]
vg_name = oncore_visit[:timePointEventDefinition][:title].sub("#{@arm_codes[arm.id]}, ", "")
day = relative_date_to_day(encounter[:activityTime][:value])
window_before = day - relative_date_to_day(encounter[:effectiveTime][:low][:value])
window_after = relative_date_to_day(encounter[:effectiveTime][:high][:value]) - day
visit_group.update_attributes(name: vg_name, day: day, window_before: window_before, window_after: window_after)
end
end
# Returns true if the SOAP message is an RPE message.
# The only difference between RPE messages and CRPC messages is that CRPC messages contain calendar information (component4's)
# and RPE messages do not have any calendar information (no component4 elements).
def is_rpe_message?
@is_rpe = oncore_endpoint_params[:plannedStudy][:component4].nil?
end
# Returns an integer representing the number of days since Jan 1, 2000
# Parameters:
# date: string date with the format "yyyymmdd" any other formats will not work.
def relative_date_to_day(date)
( Date.new(2000,1,1)..Date.new(date[0..3].to_i,date[4..5].to_i,date[6..7].to_i) ).count
end
def print_params_to_log(e=nil)
if !@is_rpe && @protocol.present?
status = e.present? ? "failure" : "success"
oncore_record = OncoreRecord.create(protocol_id: @protocol.id, calendar_version: @calendar_version, status: status)
timestamp = oncore_record.created_at
end
timestamp ||= DateTime.now
logfile = File.join(Rails.root, '/log/', "OnCore-#{Rails.env}.log")
logger = ActiveSupport::Logger.new(logfile)
logger.info "\n----------------------------------------------------------------------------------"
logger.info "RetrieveProtocolDefResponse request ---------- Timestamp: #{timestamp.to_formatted_s(:long)}"
logger.info "Params received by OncoreEndpointController:"
logger.info JSON.pretty_generate(oncore_endpoint_params.to_h)
unless e.nil?
logger.info "Error:\n"
logger.info e.message
logger.info e.backtrace.inspect
end
logger.info "----------------------------------------------------------------------------------\n"
end
def oncore_endpoint_params
params.require(:protocolDef).permit!
end
end